mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-28 22:20:35 +00:00
* docs(checkpoints): add a bilingual checkpoint storage manual Add the missing docs-site coverage for dual-mode checkpoint storage (`database.checkpoint_channel_mode: full | delta`) as a ten-chapter manual under `harness/checkpoints/` in both languages: concepts, quick start, channel modes, snapshot cadence, history cache, resume and rollback, operating checkpoints, observability, troubleshooting by symptom, and a reference of every configuration key, error message, HTTP status mapping, public symbol, load-bearing test, and pinned upstream defect. The index page is `asIndexPage`, so `/docs/harness/checkpoints` is the section itself and its `_meta.ts` lists only the nine child chapters. Discovery: `harness/_meta.ts` registers the section, the harness configuration module table gains a `database` row, and the thread persistence section of `agents-and-threads` links to the manual. Content is grounded in the code. Only the three measured values recorded in `backend/docs/runtime-guidance-details.md` are quoted; the upstream-derived 41x storage figure in the untracked delta-channel RFC is deliberately not cited. Pinned upstream defects are documented with their issue and PR numbers and the versions they are pinned at (`langgraph 1.2.9`, `langgraph-checkpoint 4.2.0`, `langgraph-checkpoint-postgres 3.1.2`). * docs(checkpoints): correct the history-cache restart requirement The history cache is built with the checkpointer at Gateway startup (`langgraph_runtime()` -> `make_checkpointer()` -> `make_checkpoint_cache()`), so editing `database.checkpoint_cache.*` does not change a running process. The manual previously marked the whole section "not restart-required", which conflated "not frozen for correctness" with "hot-reloadable" and could mislead an operator changing capacity, credentials, or retention. Distinguish the two properties everywhere the section is described: history-cache.mdx marks every field as needing a restart and adds a callout explaining the difference, reference.mdx redefines the Restart column and fixes the five rows and the effect list (adding the `asyncprov` evidence alias), troubleshooting.mdx replaces the "two keys need no restart" claim and gains a dedicated symptom entry, and observability.mdx notes that changed counters need a restart. The Chinese text is corrected in parallel. Verified: the docs link test and the docs unit suite pass, frontmatter parses for all 20 pages, and every in-page anchor resolves on the rendered English and Chinese history-cache, troubleshooting, reference, and observability pages. * docs(checkpoints): account for periodic snapshots in the storage-growth claim Every periodic snapshot stores the entire message list as of that step (`_DeltaSnapshot(ch.get())`, persisted as a checkpoint blob), so with a fixed snapshot frequency K the retained total after N turns is O(N²/K + N), not O(N). The manual claimed unconditional linear growth, which understated capacity planning — the more so because nothing in DeerFlow prunes those snapshots yet. Distinguish per-step delta writes from total retained storage in index.mdx (prose and table), modes.mdx (step-cost row), and operations.mdx; note in cadence.mdx that K is also the capacity knob that scales the snapshot bytes. The English and Chinese chapters are corrected in parallel. The reference chapter keeps the released #4292 wording as a verbatim quotation and now names the retained-storage bound beside it. Verified: `_DeltaSnapshot(ch.get())` and its blob serialization in the pinned langgraph (channels/delta.py:196-198, checkpoint/serde/types.py:19-25) confirm the premise; the docs link test and docs unit suite pass; all ten changed pages render with resolving anchors and no stale linear-growth wording. * docs(checkpoints): correct the run-error surfaces and repair two evidence pointers Review follow-ups on the checkpoint manual, both languages. Error-surface corrections: - Run creation is not always asynchronous: when a thread's run-event feed is empty but a checkpoint head exists, ensure_checkpoint_history_seeded reads that head inside start_run, where only ConflictError (409) and UnsupportedStrategyError (501) are mapped, so a mode mismatch returns a plain 500 before admission. Troubleshooting, observability and the reference route tables now scope the "does not pre-gate" claim. - The 503 symptom is retargeted. Only Gateway startup, make_lead_agent and DeerFlowClient.__init__ call the freeze helpers, so a config.yaml edit cannot make a state/history/thread route answer 503. A mode edit is injected over by the run worker and ignored until a restart; a cadence edit fails the next run with the restart-required message. The router's 503 mapping is marked defensive in modes and the reference. - The regenerate section title no longer groups the state endpoints, which map a mismatch to 409. - operations: the Textual TUI catches the mismatch through stream_actions(); only the headless --print/--json runners print a traceback. Evidence-pointer repairs: - reference: this branch's CHANGELOG insertion shifted the #5734 entry from 3032-3039 to 3040-3048; the citation is re-pointed. - reference: the #8548 row cited an untracked worktree plan file; replaced by the in-repo test anchor, and "mirror issue #8551" reworded to "related". - reference: new `agent` alias for the agent-factory freeze sites. - history-cache: the bare `deps:`/`asyncprov:` aliases are expanded to full paths; the alias legend lives only on the reference page. * docs(checkpoints): say a delta fork is accepted, not refused "Delta mode cannot fork" overstated the constraint. Nothing in the stack rejects the fork: LangGraph stores the second child, no saver checks for an existing sibling, and ensure_checkpoint_mode_compatible returns immediately when the process mode is delta. What breaks is the read — the walk replays the abandoned sibling's writes stored under the shared parent. Reproduced on the pinned stack (langgraph 1.2.9 / langgraph-checkpoint 4.2.0, AsyncSqliteSaver) by running this repo's own #4458 fixture without the linearization call: the forked resume returns ["h1", "a1", "h2", "a2", "a2-new"] instead of ["h1", "a1", "h2", "a2-new"], i.e. the superseded answer comes back. - resume-and-rollback: title/description/heading now say "cannot fork correctly"; new "Forks are accepted, not refused" subsection covering the unguarded write side. - troubleshooting: the "old answer reappears" cause states the same split. - reference: the POST /threads/{id}/state row notes that a client-supplied checkpoint_id older than the head is applied as a delta fork with no linearization — the write shape upstream tracks as #8551 (open), which the run-path linearization does not cover. * docs(checkpoints): scope the unguarded state-update fork to what the probe showed The previous commit said a mutation against an older checkpoint stores its writes under the shared parent and implied the sibling branch suffers for it. Probing the pinned stack (AsyncSqliteSaver, delta messages channel) shows: - the write does land under the shared parent and joins the write set the parent's other children walk (raw walk for a sibling includes the abandoned head's own write); - but reducer channels are written with Overwrite, which replaces the accumulated value, so the forked branch reads clean and the abandoned branch is unchanged in that shape — #8551 did not reproduce; state the masking mechanism and the upstream tracking issue, not a corruption claim. * docs(checkpoints): fix a frontmatter description that broke the docs build `resume-and-rollback.mdx` shipped `description: Delta mode cannot fork a thread correctly: a forked delta read ...` in the previous commit. The unquoted ": " turns the value into a nested mapping, so Nextra's YAML frontmatter parse failed and the e2e lane could not start its web server (YAMLParseError at line 2, column 14) — the exact failure class this manual warns docs authors about, introduced by me two commits ago. - Replace the colon with an em dash in the English description (the Chinese one uses a full-width colon and was unaffected). - Add tests/unit/content/docs-frontmatter.test.ts: every `src/content/**/*.mdx` frontmatter block must be YAML-parseable, implemented as the one rule that actually breaks it (an unquoted scalar containing ": "), because the unit suite never compiles MDX and only a build or a dev render catches it. Verified the guard fails on the broken line and passes on the fix. Verified with a clean `BETTER_AUTH_SECRET=local-dev-secret pnpm build` on this tree: all pages prerender, resume-and-rollback EN/ZH included.
389 lines
41 KiB
Plaintext
389 lines
41 KiB
Plaintext
---
|
||
title: 参考附录
|
||
description: 双模检查点存储涉及的全部配置键、常量、异常、HTTP 状态、对外与内部符号、承重测试锚点、上游缺陷固定表,以及本特性的变更记录。
|
||
---
|
||
|
||
import { Callout } from "nextra/components";
|
||
|
||
# 参考附录
|
||
|
||
`database.checkpoint_channel_mode` 涉及的每一个名称、键、字符串与测试锚点都汇总在本页。本页的用途是查阅而不是理解模型;需要概念请先读[通道模式](/docs/harness/checkpoints/modes)。
|
||
|
||
「证据」列的取值是仓库相对路径的 `path:line`,使用下表的别名。本页出现的每条事实都能追溯到这些行。
|
||
|
||
| 别名 | 路径 |
|
||
| --- | --- |
|
||
| `dbcfg` | `backend/packages/harness/deerflow/config/database_config.py` |
|
||
| `appcfg` | `backend/packages/harness/deerflow/config/app_config.py` |
|
||
| `reload` | `backend/packages/harness/deerflow/config/reload_boundary.py` |
|
||
| `cmode` | `backend/packages/harness/deerflow/runtime/checkpoint_mode.py` |
|
||
| `cstate` | `backend/packages/harness/deerflow/runtime/checkpoint_state.py` |
|
||
| `tstate` | `backend/packages/harness/deerflow/agents/thread_state.py` |
|
||
| `factory` | `backend/packages/harness/deerflow/agents/factory.py` |
|
||
| `client` | `backend/packages/harness/deerflow/client.py` |
|
||
| `worker` | `backend/packages/harness/deerflow/runtime/runs/worker.py` |
|
||
| `agent` | `backend/packages/harness/deerflow/agents/lead_agent/agent.py` |
|
||
| `goalsrc` | `backend/packages/harness/deerflow/runtime/goal.py` |
|
||
| `checkpoint_patches` | `backend/packages/harness/deerflow/checkpoint_patches.py` |
|
||
| `syncprov` | `backend/packages/harness/deerflow/runtime/checkpointer/provider.py` |
|
||
| `asyncprov` | `backend/packages/harness/deerflow/runtime/checkpointer/async_provider.py` |
|
||
| `cacheprov` | `backend/packages/harness/deerflow/runtime/checkpoint_cache/provider.py` |
|
||
| `threads` | `backend/app/gateway/routers/threads.py` |
|
||
| `thread_runs` | `backend/app/gateway/routers/thread_runs.py` |
|
||
| `runs` | `backend/app/gateway/routers/runs.py` |
|
||
| `services` | `backend/app/gateway/services.py` |
|
||
| `deps` | `backend/app/gateway/deps.py` |
|
||
| `context_usage` | `backend/app/gateway/context_usage.py` |
|
||
| `example` | `config.example.yaml` |
|
||
|
||
缺陷表引用的固定依赖集为 `langgraph 1.2.9`、`langgraph-checkpoint 4.2.0`、`langgraph-checkpoint-postgres 3.1.2`。
|
||
|
||
## 配置键
|
||
|
||
下表列出与检查点相关的全部 `database.*` 键,以及加载器实际强制执行的约束。「重启」表示运行中的进程在启动时就捕获了该值——冻结进进程状态、编译进通道表,或在构造 checkpointer 及其历史缓存这类单例时绑定——因此改动要到该进程重启后才生效。
|
||
|
||
| 键 | 类型 | 默认值 | 范围 | 重启 | 证据 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| `database.backend` | `Literal["memory", "sqlite", "postgres"]` | pydantic 默认 `"memory"`;`config.yaml` 缺该键时为 `sqlite` | `memory` \| `sqlite` \| `postgres` | 是 | `dbcfg:143-146`、`appcfg:67-70`、`appcfg:519-527` |
|
||
| `database.checkpoint_channel_mode` | `CheckpointChannelMode` = `Literal["full", "delta"]` | `"full"` | `full` \| `delta` | 是——首次使用时冻结;之后更改为不同值会抛 `CheckpointModeReconfigurationError` | `dbcfg:59`、`dbcfg:147-155`、`cmode:39-45` |
|
||
| `database.checkpoint_delta.snapshot_frequency` | `int` | `10`(`DEFAULT_CHECKPOINT_SNAPSHOT_FREQUENCY`) | `ge=1`(无上界) | 是——与模式一同冻结 | `dbcfg:61`、`dbcfg:74-84`、`cmode:53-69` |
|
||
| `database.checkpoint_graph_cache.accessor_graph_max` | `int` | `64` | `ge=1` | 否——热重载,每次淘汰检查时重读 | `dbcfg:96-99`、`services:1133-1138` |
|
||
| `database.checkpoint_cache.type` | `Literal["memory", "redis"]` | `"memory"` | `memory` \| `redis`;`redis` 仅限异步/Gateway | 是——构造 checkpointer 时捕获 | `dbcfg:113-116`、`syncprov:181-182`、`asyncprov:250-254` |
|
||
| `database.checkpoint_cache.max_entries` | `int` | `128` | `ge=0`;`0` 完全禁用缓存 | 是——构造 checkpointer 时捕获 | `dbcfg:117-121`、`cacheprov:76-84`、`deps:463` |
|
||
| `database.checkpoint_cache.redis_url` | `str \| None` | `None` | 任意 redis URL;省略时依次取 `DEER_FLOW_CHECKPOINT_CACHE_REDIS_URL`、`REDIS_URL`、`redis://localhost:6379/0` | 是——构造 checkpointer 时捕获 | `dbcfg:122-125`、`cacheprov:19-23` |
|
||
| `database.checkpoint_cache.ttl_seconds` | `int` | `86400` | `ge=0`;`0` 显式关闭过期 | 是——构造 checkpointer 时捕获 | `dbcfg:126-135` |
|
||
| `database.checkpoint_cache.key_prefix` | `str` | `""` | 任意字符串;空值则由数据库标识派生哈希 | 是——构造 checkpointer 时捕获 | `dbcfg:136-139`、`cacheprov:60-64` |
|
||
| `database.checkpoint_delta_snapshot_frequency`(已废弃的扁平键) | `int` | 无——会迁移到嵌套键 | 迁移后为 `ge=1` | 不适用 | `dbcfg:218-254` |
|
||
|
||
### 各键影响什么
|
||
|
||
- `database.checkpoint_channel_mode` 决定 `messages` 通道的存储形态:整份取值(`full`)还是 LangGraph `DeltaChannel` 哨兵加逐步写入(`delta`)。它影响一个线程的每一次读写。
|
||
- `database.checkpoint_delta.snapshot_frequency` 是 delta 模式下每 N 次逐步写入落一份完整 messages 快照的节奏。值越大检查点越小、物化越慢。`full` 模式下被忽略。
|
||
- `database.checkpoint_graph_cache.accessor_graph_max` 限制进程缓存的已编译线程状态访问器图数量,键按 assistant、通道模式与快照节奏组合。与模式和节奏不同,它不需要重启:容量不同只会改变缓存何时淘汰,不会改变图的语义。
|
||
- `database.checkpoint_cache.*` 只配置 delta 模式下的物化历史缓存。它纯属性能优化、永不冻结——没有任何正确性不变量要求跨进程一致,禁用缓存时结果也完全相同——但它**不是热重载**:缓存对象在启动时随 checkpointer 一起构造(`deps:463`、`asyncprov:250-254`),而 `reload` 把整个 `database` 段登记为 startup-only(`reload:47`),所以改动其中任何一项都需要重启。memory 后端受 `max_entries` 约束,redis 后端受 `ttl_seconds` 与服务器自身 maxmemory 策略约束。
|
||
- `database.backend` 决定由哪种 saver 承载线程(`InMemorySaver`、SQLite 或 Postgres)。模式语义位于已编译的通道表而不是 saver,所以三种后端都支持两种模式。
|
||
|
||
两个冻结键的 `Field(description=...)` 原文值得读一遍,因为它写明了跨进程规则:
|
||
|
||
```text
|
||
"Checkpoint representation for accumulating channels. 'full' preserves full-value message checkpoints;
|
||
'delta' uses LangGraph DeltaChannel for messages. Restart is required, and all processes sharing one
|
||
checkpoint database must use the same value."
|
||
```
|
||
|
||
```text
|
||
"DeltaChannel snapshot cadence: a full messages snapshot is stored every N per-step writes
|
||
(higher = smaller checkpoints, slower materialization). Restart is required, and all processes
|
||
sharing one checkpoint database must use the same value."
|
||
```
|
||
|
||
### 校验行为
|
||
|
||
| 输入 | 结果 | 证据 |
|
||
| --- | --- | --- |
|
||
| `checkpoint_channel_mode` 取 `full`/`delta` 之外的值(例如 `auto`) | 加载配置时抛 pydantic `ValidationError` | `backend/tests/test_app_config_reload.py:118-120` |
|
||
| `snapshot_frequency` 取 `0` 或 `-1` | 抛 pydantic `ValidationError`(约束 `ge=1`) | `dbcfg:76`、`backend/tests/test_app_config_reload.py:131-134` |
|
||
| 已废弃的扁平键取 `0` 或 `-1` | 取值迁移到嵌套键后同样抛 `ValidationError` | `backend/tests/test_app_config_reload.py:154-157` |
|
||
| `database:` 下的未知键 | 被忽略(pydantic `extra="ignore"`),这正是旧键兼容垫片存在的原因 | `dbcfg:224-227` |
|
||
| 扁平键与 `checkpoint_delta.snapshot_frequency` 同时设置 | 嵌套键胜出,并记一条警告 | `dbcfg:218-240` |
|
||
|
||
### 已废弃的扁平键与迁移告警
|
||
|
||
旧的顶层键 `database.checkpoint_delta_snapshot_frequency` 仍被 pydantic 的 `@model_validator(mode="before")`(名为 `_migrate_legacy_snapshot_frequency`)接受。它只会记录以下三条字符串之一,全部对运维可见:
|
||
|
||
| 条件 | 原文 | 证据 |
|
||
| --- | --- | --- |
|
||
| 两个键都设置,且 `checkpoint_delta` 是 dict | `Both database.checkpoint_delta_snapshot_frequency (deprecated) and database.checkpoint_delta.snapshot_frequency are set; the nested key wins.` | `dbcfg:236-238` |
|
||
| `checkpoint_delta` 是已构造好的 `CheckpointDeltaConfig` 对象 | `Ignoring deprecated database.checkpoint_delta_snapshot_frequency because database.checkpoint_delta is already set.` | `dbcfg:246-248` |
|
||
| 旧值被前移 | `database.checkpoint_delta_snapshot_frequency is deprecated; use database.checkpoint_delta.snapshot_frequency instead. Carried the legacy value (%r) forward.` | `dbcfg:250-253` |
|
||
|
||
### 环境变量
|
||
|
||
没有任何环境变量能选择检查点模式或快照节奏,两者只来自 `config.yaml`。本特性唯一相关的环境变量是历史缓存 redis URL 的回退链(`DEER_FLOW_CHECKPOINT_CACHE_REDIS_URL`、`REDIS_URL`、`redis://localhost:6379/0`),见 `cacheprov:19-23`。配置值里的 `$VAR` 插值由 `AppConfig.resolve_env_variables()` 在构造 `DatabaseConfig` 之前统一处理,因此 `DatabaseConfig` 自身不做任何环境处理(`dbcfg:13-22`、`appcfg:442`)。
|
||
|
||
### 配置版本
|
||
|
||
该 schema 变更随 `config_version 27` 发布;`config.example.yaml` 当前是 `config_version: 47`,缺少版本号按 `0` 处理(`example:23`、`appcfg:531-575`)。过旧的 `config.yaml` 会记录:
|
||
|
||
```text
|
||
Your config.yaml (version %d) is outdated — the latest version is %d. Run `make config-upgrade` to merge new fields into your config.
|
||
```
|
||
|
||
`make config-upgrade` 会以安全默认值 `full` 合并新键(`appcfg:570-575`、`backend/tests/test_config_version.py:151-203`)。通用升级流程见[配置](/docs/harness/configuration)。
|
||
|
||
## 常量与键
|
||
|
||
| 符号 | 取值 | 所在模块 | 证据 |
|
||
| --- | --- | --- | --- |
|
||
| `CheckpointChannelMode` | `Literal["full", "delta"]` | `deerflow.config.database_config` | `dbcfg:59` |
|
||
| `DEFAULT_CHECKPOINT_SNAPSHOT_FREQUENCY` | `10` | `deerflow.config.database_config` | `dbcfg:61` |
|
||
| `INTERNAL_CHECKPOINT_MODE_KEY` | `"__deerflow_checkpoint_channel_mode"`(`RunnableConfig` 的 `configurable` 键) | `deerflow.runtime.checkpoint_mode` | `cmode:18` |
|
||
| `CHECKPOINT_MODE_METADATA_KEY` | `"deerflow_checkpoint_channel_mode"`(检查点 metadata 键;写入的值为 `"delta"`) | `deerflow.runtime.checkpoint_mode` | `cmode:19`、`cmode:86` |
|
||
| `counters_since_delta_snapshot` | LangGraph 自有的 metadata 键;只要其值是含 `"messages"` 的 dict,即使没有 DeerFlow 标记也按 delta 处理 | 上游 `langgraph-checkpoint`;检测位于 `deerflow.runtime.checkpoint_mode` | `cmode:97-98` |
|
||
|
||
标记语义集中在一处:`inject_checkpoint_mode(config, mode)` 总是写入 `config["configurable"]["__deerflow_checkpoint_channel_mode"]`;delta 模式下还会写入 `config["metadata"]["deerflow_checkpoint_channel_mode"] = "delta"`,而 full 模式下会**移除**该 metadata 键,所以缺失即 full,特性上线前的检查点无需迁移(`cmode:81-88`)。快照节奏被刻意**不**写进检查点 metadata,因为模式标记契约与 full→delta 迁移语义都不随频率取值改变(`cmode:53-60`)。
|
||
|
||
## 异常与 HTTP 状态
|
||
|
||
| 类 | 基类 | docstring | 证据 |
|
||
| --- | --- | --- | --- |
|
||
| `CheckpointModeMismatchError` | `RuntimeError` | `"Raised before a full-mode graph reads a Delta checkpoint."` | `cmode:22-23` |
|
||
| `CheckpointModeReconfigurationError` | `RuntimeError` | `"Raised when a process attempts to hot-switch its persistence mode."` | `cmode:26-27` |
|
||
|
||
模式机制及其守卫抛出的原文消息:
|
||
|
||
| 原文消息 | 抛出位置 | 证据 |
|
||
| --- | --- | --- |
|
||
| `checkpoint_channel_mode is restart-required and cannot change in a running process` | `freeze_checkpoint_channel_mode` → `CheckpointModeReconfigurationError` | `cmode:44` |
|
||
| `checkpoint_delta.snapshot_frequency is restart-required and cannot change in a running process` | `freeze_checkpoint_snapshot_frequency` → `CheckpointModeReconfigurationError` | `cmode:68` |
|
||
| `snapshot frequency must be positive` | `freeze_checkpoint_snapshot_frequency` → `ValueError`,仅当绕过 pydantic 传入非正值 | `cmode:63-64` |
|
||
| `Thread requires delta mode; materialize and convert its checkpoints before using full mode.` | `CheckpointModeMismatchError`,由 `raise_if_snapshot_incompatible` 与 `raise_if_checkpoint_tuple_incompatible` 抛出,并在降级网关访问器中重复 | `cmode:123`、`cmode:129`、`services:1229` |
|
||
| `database.checkpoint_cache.type 'redis' is not supported on the sync checkpointer path (TUI/embedded); use 'memory'.` | `_wrap_sync_if_delta` 中的 `ValueError` | `syncprov:181-182` |
|
||
| `f"Unknown checkpoint cache type: {config.type!r}"` | `make_checkpoint_cache` 中的 `ValueError` | `cacheprov:102` |
|
||
| `create_deerflow_agent does not support checkpoint_channel_mode='delta' with a checkpointer: persisted graphs built here bypass checkpoint mode marker injection and the fail-closed compatibility gate (see deerflow.runtime.checkpoint_mode), so a mixed-mode store would silently corrupt thread state. Use the guarded application paths (make_lead_agent or DeerFlowClient) for delta persistence; delta without a checkpointer is ephemeral and allowed.` | `create_deerflow_agent` 中的 `ValueError` | `factory:135-141` |
|
||
|
||
### HTTP 状态映射
|
||
|
||
只有 threads 路由导入这些异常。它注册门禁元组 `_CHECKPOINT_MODE_ERRORS = (CheckpointModeMismatchError, CheckpointModeReconfigurationError)`(`threads:82`),映射如下。响应体是 FastAPI 默认的 `{"detail": "<string>"}`。
|
||
|
||
| 条件 | 状态码 | `detail` | 证据 |
|
||
| --- | --- | --- | --- |
|
||
| `CheckpointModeMismatchError` | `409` | `f"Thread {thread_id}: {exc}"`,即 `Thread <thread_id>: Thread requires delta mode; materialize and convert its checkpoints before using full mode.` | `threads:105-106` |
|
||
| `CheckpointModeReconfigurationError` | `503` | `str(exc)` | `threads:107` |
|
||
|
||
路由给出的理由:不匹配说明线程持久化的检查点与进程冻结的模式冲突(运维可处理,409);重新配置说明进程自身正处于模式切换中(暂时性,503)(`threads:99-102`)。`503` 这一半属于防御性代码 —— 这些路由中没有任何路径会抛出它。冻结辅助函数只被 Gateway 启动(`deps:451-452`)、`agent:835,839` 与 `client:230-231` 调用,而没有任何状态、历史或线程路由会构建 agent:`checkpoint_channel_mode` 的改动会被 run worker 注入的值覆盖(`worker:1018-1019`,优先采用于 `agent:831-835`),在重启前被忽略;`snapshot_frequency` 的改动则会让下一次 run 失败(`worker:1450-1462`),而不是返回某个状态码。
|
||
|
||
### 返回 409 的路由
|
||
|
||
| 路由 | 处理器 | 说明 | 证据 |
|
||
| --- | --- | --- | --- |
|
||
| `GET /api/threads/{thread_id}` | `get_thread` | 在 `404 Thread <id> not found` 之前检查 | `threads:1276,1291-1292,1298-1299,1303` |
|
||
| `GET /api/threads/{thread_id}/state` | `get_thread_state` | | `threads:1446,1454-1455,1458-1459` |
|
||
| `POST /api/threads/{thread_id}/state` | `update_thread_state` | 调用方传入的 `checkpoint_id` 若比 head 更旧,会作为一次增量分叉被直接应用,没有线性化 —— 见[分叉会被接受,而不是被拒绝](/docs/harness/checkpoints/resume-and-rollback#分叉会被接受而不是被拒绝) | `threads:1495,1559-1560` |
|
||
| `POST /api/threads/{thread_id}/history` | `get_thread_history` | | `threads:1695,1715-1716,1907-1908` |
|
||
| `POST /api/threads/{thread_id}/branches` | `branch_thread` → `_branch_thread_with_reservation` | detail 中的线程 id 是源线程;无测试覆盖该路由 | `threads:953,231-232,298-299,1105-1106` |
|
||
| `POST /api/threads/{thread_id}/compact` | `compact_thread` | 压缩被禁用时同样返回 `409`;无测试覆盖该路由 | `threads:1399,1413-1414,1429-1430,1431-1432` |
|
||
|
||
### 不匹配异步失败的路由
|
||
|
||
| 路由 | 即时响应 | 失败载荷 | 证据 |
|
||
| --- | --- | --- | --- |
|
||
| `POST /api/threads/{thread_id}/runs` | `200` run 记录,无前置门禁 —— 但当线程的 run event 流为空时,`ensure_checkpoint_history_seeded` 会先在 `start_run` 内物化头部,此时返回 `500` | run 记录稍后变为 `status: error`,`error` 为不匹配原文 | `thread_runs:945-960`、`worker:1018-1030,1044-1047`、`worker:1450-1462`、`services:1503-1520,2055` |
|
||
| `POST /api/threads/{thread_id}/runs/stream` | `200` `text/event-stream` | 一帧 `event: error`,其 `data` 为 `{"message": <不匹配原文>, "name": "CheckpointModeMismatchError"}` | `worker:1466-1472`、`services:2374-2378` |
|
||
| `POST /api/threads/{thread_id}/runs/wait` | `200` JSON,从不返回 `409` | `{"status": "error", "error": "Thread requires delta mode; materialize and convert its checkpoints before using full mode."}` | `thread_runs:1018-1076` |
|
||
| `POST /api/runs/stream`、`POST /api/runs/wait`(无状态) | 同上 | 同样的 `{"status", "error"}` 载荷 | `runs:59-91` |
|
||
|
||
### 未做模式映射的路由
|
||
|
||
| 路由 | 实际行为 | 证据 |
|
||
| --- | --- | --- |
|
||
| `POST /api/threads/{thread_id}/runs/regenerate/prepare` | `500`,`detail: "Failed to read latest checkpoint"`——访问器构建位于任何 `try` 之外 | `thread_runs:747,749-752` |
|
||
| 同一路由的谱系/历史扫描 | `500`,`detail: "Failed to inspect checkpoint history"` | `thread_runs:679-681` |
|
||
| `POST /api/threads/{thread_id}/runs/edit-regenerate/prepare` | 同样两条 `500` 路径 | `thread_runs:838-841`、`thread_runs:633-697` |
|
||
| `POST /api/threads/{thread_id}/runs`、`/runs/stream`、`/runs/wait` 及无状态 `/api/runs/*`,当线程的 run event 流为空且存在检查点头部时 | `500`:`ensure_checkpoint_history_seeded` 在准入前通过受门禁的访问器读取该头部,而 `start_run` 只映射 `ConflictError`(`409`)与 `UnsupportedStrategyError`(`501`) | `services:1503-1520`、`services:2055`、`services:2126-2129` |
|
||
| `GET /api/threads/{thread_id}/token-usage` | `200`,`context_usage: null`,仅记一条警告日志 | `context_usage.py:73-78`、`thread_runs:1872-1873` |
|
||
| `GET` / `PUT` / `DELETE /api/threads/{thread_id}/goal` | 完全没有门禁:直接对 `channel_values["goal"]` 调用 `checkpointer.aget_tuple` / `aput` | `threads:1329-1383`、`goalsrc:442-453,468-477,480-542` |
|
||
| `DELETE /api/threads/{thread_id}`、`POST /search`、`PATCH`、`POST /move` | 只操作元数据库,对模式无感 | `threads:706,1173,1216,1255` |
|
||
|
||
### 没有任何上报面
|
||
|
||
没有任何健康、就绪、特性、能力或控制台字段会上报模式或节奏。响应模型、响应头、事件字段也都不携带它:仅有的自定义 SSE 头是 `Cache-Control`、`Connection`、`X-Accel-Buffering`、`Content-Location`。模式只在服务端被消费,经由 `app.state.checkpoint_channel_mode`、`app.state.checkpoint_snapshot_frequency` 与 `RunContext`(`deps:451-452,767-768`、`services:1093,1283-1312`)。一处潜在例外:`GET /api/threads/{thread_id}` 与 `POST /api/threads/{thread_id}/history` 的响应 `metadata` 会剥离其他内部键,但不剥离 delta 标记与 `counters_since_delta_snapshot`,因此它们可能透传到客户端(`threads:1308-1315`、`threads:1890-1900`)。SSE 的 `metadata` 帧只带 `{"run_id", "thread_id"}`(`worker:1067-1074`)。见[可观测性](/docs/harness/checkpoints/observability)。
|
||
|
||
## 对外符号
|
||
|
||
消费者可以依赖的导入路径。这里列出的都是 Python 导入;路由契约见上文「异常与 HTTP 状态」一节。
|
||
|
||
| 符号 | 导入路径 | 作用 | 证据 |
|
||
| --- | --- | --- | --- |
|
||
| `CheckpointChannelMode` | `deerflow.config.database_config` | 模式字面量 | `dbcfg:59` |
|
||
| `DEFAULT_CHECKPOINT_SNAPSHOT_FREQUENCY` | `deerflow.config.database_config` | 默认节奏 `10` | `dbcfg:61` |
|
||
| `CheckpointDeltaConfig` | `deerflow.config.database_config` | delta 调优模型(`snapshot_frequency`) | `dbcfg:64` |
|
||
| `CheckpointGraphCacheConfig` | `deerflow.config.database_config` | 图缓存上限模型(`accessor_graph_max`) | `dbcfg:86` |
|
||
| `CheckpointCacheConfig` | `deerflow.config.database_config` | 历史缓存策略模型 | `dbcfg:103` |
|
||
| `DatabaseConfig` | `deerflow.config.database_config` | 整个 `database:` 段 | `dbcfg:142` |
|
||
| `resolve_checkpoint_graph_cache_max(database_config, field_name, default)` | `deerflow.config.database_config` | 容错的热重载上限读取器 | `dbcfg:45-57` |
|
||
| `INTERNAL_CHECKPOINT_MODE_KEY` | `deerflow.runtime.checkpoint_mode` | `configurable` 键 | `cmode:18` |
|
||
| `CHECKPOINT_MODE_METADATA_KEY` | `deerflow.runtime.checkpoint_mode` | 检查点 metadata 键 | `cmode:19` |
|
||
| `CheckpointModeMismatchError` | `deerflow.runtime.checkpoint_mode` | 失败关闭的跨模式异常 | `cmode:22` |
|
||
| `CheckpointModeReconfigurationError` | `deerflow.runtime.checkpoint_mode` | 需要重启却被改动的异常 | `cmode:26` |
|
||
| `frozen_checkpoint_channel_mode()` | `deerflow.runtime.checkpoint_mode` | 读取已冻结的模式或 `None` | `cmode:34-36` |
|
||
| `freeze_checkpoint_channel_mode(mode)` | `deerflow.runtime.checkpoint_mode` | 首次调用时冻结,之后拒绝变更 | `cmode:39-45` |
|
||
| `frozen_checkpoint_snapshot_frequency()` | `deerflow.runtime.checkpoint_mode` | 读取已冻结的节奏或 `None` | `cmode:48-50` |
|
||
| `freeze_checkpoint_snapshot_frequency(n)` | `deerflow.runtime.checkpoint_mode` | 冻结节奏;非正值抛 `ValueError` | `cmode:53-69` |
|
||
| `resolve_checkpoint_snapshot_frequency(n=None)` | `deerflow.runtime.checkpoint_mode` | 显式参数 → 进程冻结值 → 默认值 | `cmode:72-78` |
|
||
| `inject_checkpoint_mode(config, mode)` | `deerflow.runtime.checkpoint_mode` | 写入 `configurable` 键与 metadata 标记 | `cmode:81-88` |
|
||
| `checkpoint_metadata_uses_delta(metadata)` | `deerflow.runtime.checkpoint_mode` | 标记/计数器识别 | `cmode:91-98` |
|
||
| `checkpoint_tuple_uses_delta(tuple)` | `deerflow.runtime.checkpoint_mode` | 元组级识别 | `cmode:101-104` |
|
||
| `state_snapshot_uses_delta(snapshot)` | `deerflow.runtime.checkpoint_mode` | 快照级识别 | `cmode:107-111` |
|
||
| `raise_if_snapshot_incompatible(snapshot, mode)` | `deerflow.runtime.checkpoint_mode` | 读取门禁 | `cmode:114-123` |
|
||
| `raise_if_checkpoint_tuple_incompatible(tuple, mode)` | `deerflow.runtime.checkpoint_mode` | 裸元组门禁 | `cmode:126-129` |
|
||
| `ensure_checkpoint_mode_compatible(checkpointer, config, mode)` | `deerflow.runtime.checkpoint_mode` | 同步写前门禁 | `cmode:132-140` |
|
||
| `aensure_checkpoint_mode_compatible(checkpointer, config, mode)` | `deerflow.runtime.checkpoint_mode` | 异步写前门禁 | `cmode:143-146` |
|
||
| `ThreadState` | `deerflow.agents.thread_state` | full 模式状态 schema | `tstate:280` |
|
||
| `DeltaThreadState` | `deerflow.agents.thread_state` | 默认节奏下的 delta schema | `tstate:396-397,423-426` |
|
||
| `delta_messages_field(snapshot_frequency=DEFAULT_CHECKPOINT_SNAPSHOT_FREQUENCY)` | `deerflow.agents.thread_state` | `Annotated[list[AnyMessage], DeltaChannel(merge_message_writes, snapshot_frequency=…)]` | `tstate:385-390` |
|
||
| `DELTA_MESSAGES_FIELD` | `deerflow.agents.thread_state` | 模块级默认节奏 delta 字段 | `tstate:393` |
|
||
| `merge_message_writes(state, writes)` | `deerflow.agents.thread_state` | 线性时间、等价 `add_messages` 的 delta 归并器 | `tstate:328-382` |
|
||
| `get_thread_state_schema(mode, snapshot_frequency=None)` | `deerflow.agents.thread_state` | 非 delta 返回 `ThreadState`,delta 返回缓存 schema | `tstate:416-419` |
|
||
| `adapt_state_schema_for_mode(schema, mode, snapshot_frequency=None)` | `deerflow.agents.thread_state` | 中间件/状态 schema 适配 | `tstate:437-440` |
|
||
| `normalize_middleware_state_schemas(middleware, mode, snapshot_frequency=None)` | `deerflow.agents.thread_state` | 返回带适配 `state_schema` 副本的中间件列表 | `tstate:454-467` |
|
||
| `THREAD_STATE_REDUCER_FIELDS` | `deerflow.agents.thread_state` | 归并器通道的 frozenset(含 `"messages"`),用于决定是否包 `Overwrite` | `tstate:400-413`、`threads:1065-1066` |
|
||
| `create_deerflow_agent(..., checkpoint_channel_mode="full", checkpoint_snapshot_frequency=None, ...)` | `deerflow.agents.factory` | 对外 agent 工厂;delta 加 checkpointer 会被拒绝 | `factory:66-77`、`factory:135-141` |
|
||
| `CheckpointStateAccessor`、`build_state_mutation_graph` | `deerflow.runtime`(包级再导出) | 访问器收口点与变更图 | `deerflow/runtime/__init__.py:8,21-22` |
|
||
| `ThreadState`、`DeltaThreadState`、`SandboxState` | `deerflow.agents`(包级再导出) | 惰性导出的状态 schema | `agents/__init__.py:14-16,40-47` |
|
||
| `CheckpointStateAccessor.bind(graph, checkpointer, *, store=None, mode="full")` | `deerflow.runtime.checkpoint_state` | 绑定 graph、saver 与模式 | `cstate:112-134` |
|
||
| `build_state_mutation_graph(as_node, mode, state_schema=None, *, snapshot_frequency=None)` | `deerflow.runtime.checkpoint_state` | 面向 `Overwrite` 式写入的纯状态写入图 | `cstate:39-65` |
|
||
|
||
<Callout type="warning">
|
||
delta 模式下裸读 saver 看到的是哨兵而不是消息。消费者必须通过
|
||
`CheckpointStateAccessor` 读取状态,而不是直接调用 checkpointer;
|
||
访问器的 docstring 明确写了这一点。
|
||
</Callout>
|
||
|
||
## 内部符号
|
||
|
||
以下都是实现细节。不要导入它们,也不要依赖其名称或形状。
|
||
|
||
| 符号 | 为何不是 API | 证据 |
|
||
| --- | --- | --- |
|
||
| `_frozen_checkpoint_channel_mode`、`_frozen_checkpoint_snapshot_frequency` | 模块级全局变量,只通过 freeze/frozen 辅助函数触碰;测试用 monkeypatch 重置 | `cmode:30-31`、`backend/tests/conftest.py:96-111` |
|
||
| `_delta_thread_state_schema` | `@cache` 装饰的内部构建器;生成 `DeltaThreadState_f{freq}` | `tstate:422-434` |
|
||
| `_adapt_state_schema_for_delta` | `@cache` 装饰的内部适配器;生成 `Delta{module_underscored}_{Name}_f{freq}` | `tstate:443-451` |
|
||
| `_resolve_snapshot_frequency` | 内部惰性导入解析器 | `tstate:25-33` |
|
||
| `_STATE_ACCESSOR_GRAPH_CACHE_MAX` | 代码内回退常量 `64`,仅在配置取值不可用时使用 | `services:1127,1133-1138` |
|
||
| `_RawCheckpointReadAccessor`、`_RawCheckpointSnapshot` | full 模式的降级只读回退 | `services:1199-1263` |
|
||
| `_checkpoint_mode_http_error`、`_CHECKPOINT_MODE_ERRORS` | 路由内部的 HTTP 映射 | `threads:82,96-107` |
|
||
| `_wrap_sync_if_delta` | 同步 provider 内部实现 | `syncprov:164-192` |
|
||
| `_migrate_legacy_snapshot_frequency` | pydantic `mode="before"` 校验器,标注为 `-- Legacy key migration (not user-configured) --` | `dbcfg:216-254` |
|
||
| `_finish_state_mutation` | 空操作写入节点 | `cstate:32-33` |
|
||
|
||
## 承重测试
|
||
|
||
读者可以运行或引用的测试,即这份可执行契约。路径均位于 `backend/tests/` 下。
|
||
|
||
### 模式门禁、冻结与配置
|
||
|
||
- `test_checkpoint_mode.py`(冻结、识别、门禁)——`backend/packages/harness/deerflow/runtime/AGENTS.md:133` 中列为模式门禁的模块
|
||
- `test_checkpoint_mode.py::test_yaml_mode_change_is_rejected_when_graph_is_reconstructed`
|
||
- `test_checkpoint_mode.py::test_delta_mode_accepts_plain_full_checkpoint`
|
||
- `test_checkpoint_mode.py::test_gateway_runtime_rejects_mode_different_from_frozen_process`
|
||
- `test_threads_checkpoint_mode.py`(路由层模式行为)
|
||
- `test_threads_checkpoint_mode.py::test_delta_snapshot_frequency_controls_snapshot_cadence`
|
||
- `test_gateway_checkpoint_mode.py`(双模式端到端一致性)
|
||
- `test_app_config_reload.py::test_checkpoint_channel_mode_rejects_unknown_value`
|
||
- `test_config_version.py`(配置版本迁移)
|
||
|
||
### 物化、存储形态与迁移
|
||
|
||
- `test_delta_channel_checkpointers.py::test_full_to_delta_migration_replays_on_same_thread`
|
||
- `test_delta_channel_checkpointers.py::test_materialization_is_deterministic_and_message_ids_are_stable`
|
||
- `test_delta_channel_checkpointers.py::test_delta_storage_shape_and_snapshot_cadence`
|
||
- `test_delta_channel_checkpointers.py::test_non_delta_writers_preserve_delta_messages_and_markers`
|
||
- `test_delta_channel_checkpointers.py::test_long_chain_history_survives_pagination`
|
||
- `test_delta_channel_checkpointers.py::test_parallel_superstep_replay_matches_live_write_order`
|
||
- `test_delta_channel_state.py::test_mode_selects_expected_state_schema`
|
||
- `test_delta_channel_state.py::test_compiled_delta_graph_bakes_configured_snapshot_frequency`
|
||
- `test_delta_channel_state.py::test_production_message_forms_keep_assigned_ids_across_delta_replay`
|
||
- `test_delta_channel_state.py::test_delta_normalization_compiles_stable_channel_without_mutating_middleware`
|
||
|
||
`test_long_chain_history_survives_pagination` 与 `test_parallel_superstep_replay_matches_live_write_order` 断言的是正确契约,在固定依赖仍带缺陷时触发 skip;依赖升级后它们会成为实际门禁。
|
||
|
||
### 历史缓存
|
||
|
||
- `test_cached_history_saver.py::test_composition_matches_full_walk_and_avoids_it`
|
||
- `test_cached_history_saver.py::test_latest_config_caches_under_resolved_checkpoint_id`
|
||
- `test_cached_history_saver.py::test_recursive_resolve_caches_intermediate_levels`
|
||
- `test_cached_history_saver.py::test_deep_cold_chain_delegates_one_inner_walk_at_depth_limit`
|
||
- `test_cached_history_saver.py::test_cold_resolve_never_delegates_to_inner_history`
|
||
- `test_cached_history_saver.py::test_sync_cold_resolve_never_delegates_to_inner_history`
|
||
- `test_cached_history_saver.py::test_snapshot_parent_composes_without_parent_history_lookup`
|
||
- `test_cached_history_saver.py::test_root_checkpoint_history_is_empty_writes`
|
||
- `test_cached_history_saver.py::test_eviction_falls_back_to_walk_but_stays_correct`
|
||
- `test_cached_history_saver.py::test_sync_path_matches_async`
|
||
- `test_cached_history_saver.py::test_stats_expose_composition_counters`
|
||
- `test_cached_history_saver.py::test_adelete_thread_purges_only_that_threads_cache_entries`
|
||
- `test_cached_history_saver.py::test_sync_delete_thread_purges_cache_entries`
|
||
- `test_cached_history_saver.py::test_prune_purges_rewritten_threads_cache_entries`
|
||
- `test_cached_history_saver.py::test_delete_for_runs_delegates_without_cache_purge`
|
||
- `test_cached_history_saver_integration.py::test_sequential_run_composes_without_inner_walks`
|
||
- `test_cached_history_saver_integration.py::test_cache_disabled_parity`
|
||
- `test_cached_history_saver_integration.py::test_branch_divergence_no_cross_contamination`
|
||
- `test_cached_history_saver_integration.py::test_interrupt_resume_appended_head_writes`
|
||
- `test_cached_history_saver_integration.py::test_eviction_only_costs_performance`
|
||
- `test_cached_history_saver_integration.py::test_rollback_supersede_does_not_pollute`
|
||
- `test_cached_history_saver_integration.py::test_full_to_delta_migration_under_cache_states`
|
||
|
||
### 恢复线性化与回滚
|
||
|
||
- `test_run_worker_delta_resume.py::test_linearizes_a_delta_resume_onto_the_head`
|
||
- `test_run_worker_delta_resume.py::test_linearization_restores_all_selected_state_and_clears_newer_channels`
|
||
- `test_run_worker_delta_resume.py::test_linearized_resume_preserves_selected_checkpoint_agent_binding`
|
||
- `test_run_worker_delta_resume.py::test_regenerating_in_a_branched_thread_does_not_resurrect_the_old_answer`
|
||
- `test_run_worker_delta_resume.py::test_run_agent_streams_from_the_linearized_delta_resume`
|
||
- `test_run_worker_delta_resume.py::test_run_agent_serializes_resume_preparation_with_checkpoint_writes`
|
||
- `test_run_worker_delta_resume.py::test_cancelled_delta_resume_rolls_back_to_pre_linearization_head`
|
||
- `test_run_worker_delta_resume.py::test_full_mode_keeps_the_fork`
|
||
- `test_run_worker_delta_resume.py::test_ordinary_run_without_a_checkpoint_selector_is_untouched`
|
||
- `test_run_worker_delta_resume.py::test_selecting_the_head_is_already_linear`
|
||
- `test_run_worker_delta_resume.py::test_unmaterializable_resume_state_fails_closed`
|
||
- `test_run_worker_rollback.py::test_rollback_linearizes_delta_restore_onto_cancelled_head`
|
||
- `test_run_worker_rollback.py::test_rollback_linearizes_delta_restore_sqlite_reopen`
|
||
- `test_run_worker_rollback.py::test_rollback_restores_pre_run_pending_writes_for_delta_checkpoints`
|
||
- `test_run_worker_rollback.py::test_rollback_preserves_middleware_contributed_channels[delta]`
|
||
- `test_run_worker_rollback.py::test_rollback_preserves_agent_binding_for_manual_compaction[delta]`
|
||
- `test_run_worker_rollback.py::test_run_agent_marks_rollback_unusable_when_capture_fails`
|
||
|
||
### 保留
|
||
|
||
- `test_checkpoint_retention_contract.py::test_growth_baseline_full_vs_delta`
|
||
- `test_checkpoint_retention_contract.py::test_deleting_branch_ancestor_breaks_lineage_loudly`
|
||
- `test_checkpoint_retention_contract.py::test_deleting_explicit_resume_target_breaks_resume`
|
||
- `test_checkpoint_retention_contract.py::test_pending_writes_are_retained_state_not_garbage`
|
||
- `test_checkpoint_retention_contract.py::test_leaf_duration_checkpoint_deletion_is_safe`
|
||
- `test_checkpoint_retention_contract.py::test_leaf_sibling_branch_deletion_is_safe`
|
||
|
||
### 补丁
|
||
|
||
- `test_checkpoint_patches.py::test_binop_overwrite_patch_is_active`
|
||
- `test_checkpoint_patches.py::test_binop_overwrite_patch_stands_down_when_upstream_fixed`
|
||
|
||
## 上游缺陷固定表
|
||
|
||
状态对应固定依赖集(`langgraph 1.2.9`、`langgraph-checkpoint 4.2.0`、`langgraph-checkpoint-postgres 3.1.2`)。每条本地测试断言的都是正确契约,缺陷存在期间触发 skip,因此后续依赖升级会把它变成实际门禁;有缺陷的行为从不被当作正确行为断言。
|
||
|
||
| 编号 | 种类 / 状态 | 触发场景 | 本地应对 | 固定版本 | 证据 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| `#4380` | DeerFlow issue;已在本地打补丁 | 向空的 Union 类型 `BinaryOperatorAggregate` 通道做替换式写入会把 `Overwrite` 包装器持久化;下一次读取以 `TypeError: 'Overwrite' object is not subscriptable` 崩溃 | `checkpoint_patches.py` 中的探针守卫补丁;补丁测试;`test_threads_router.py` 中的分支/状态更新锚点 | `langgraph 1.2.9` | `checkpoint_patches:91-107`、`backend/tests/test_checkpoint_patches.py:31-95` |
|
||
| `#8526`(修复上游 `#8384`),以及 postgres `#8535` | 上游 langgraph PR;2026-08-07 合并;`langgraph-checkpoint` 4.2.0 修复 | 线程从 `BinaryOperatorAggregate` 切换到 `DeltaChannel` 后,迁移后的第一次写入能从实时 `invoke` 返回值中看到,却从之后每次读取中消失;postgres 侧在没有 seed 时走到根并重放全部内容 | 删除本地 `InMemorySaver` 补丁;依赖下限 `langgraph-checkpoint>=4.2.0,<5.0`;回归门禁 `test_delta_channel_checkpointers.py::test_full_to_delta_migration_replays_on_same_thread`;禁止再引入版本守卫式 saver 补丁 | `langgraph-checkpoint 4.2.0` | `checkpoint_patches:11-17`、`backend/packages/harness/pyproject.toml:49-56`、`CHANGELOG.md:3040-3048` |
|
||
| `#8382` | 上游 langgraph issue;**open**;修复 PR `#8544` 未合并,无发布版本 | 并行 super-step 写入的 delta 重放顺序与实时执行顺序不一致:实时 `invoke` 返回按路径排序的 `[a..h]`,`get_state` 重放为 `[f,b,g,h,a,c,d,e]`(纯置换);在 memory、sqlite、postgres 上均可复现 | 触发 skip 并点名该 issue;`test_delta_channel_checkpointers.py::test_parallel_superstep_replay_matches_live_write_order` | `langgraph-checkpoint 4.2.0` | `test_delta_channel_checkpointers.py:419,440-460` |
|
||
| `#8448` | 上游 langgraph issue;**open**;修复 PR `#8556` 未合并 | 目标检查点不在第一页分页结果中时,`PostgresSaver.get_delta_channel_history` 会永久污染游标,导致老检查点物化为空;只有 Postgres 会对 stage-1 扫描分页 | memory/sqlite 上断言保持生效(与 `InMemorySaver` 预言机做差分);Postgres 触发 skip;`test_delta_channel_checkpointers.py::test_long_chain_history_survives_pagination` | `langgraph-checkpoint-postgres 3.1.2` | `test_delta_channel_checkpointers.py:459` |
|
||
| `#8548` | 上游 langgraph PR;**open**;修复上游 `#8443`,相关 issue `#8551` 仍在等它 | 不把被放弃的分支重放进 `DeltaChannel` 分叉 | 未合并期间本地线性化不能移除:保留 `_linearize_delta_checkpoint_resume` | `langgraph 1.2.9` | `worker:2267-2279`、`test_run_worker_delta_resume.py::test_linearizes_a_delta_resume_onto_the_head` |
|
||
| `#4458` | DeerFlow issue;分叉缺陷对用户可见的症状 | 在分叉线程中重新生成后,重载时旧助手消息会与新回答并排复活;在 postgres、sqlite 与内存 saver 上均可复现,且当种子行共用一个 run id 时还会破坏分支历史 | 线性化加按轮次播种的 run id;`test_run_worker_delta_resume.py::test_regenerating_in_a_branched_thread_does_not_resurrect_the_old_answer` | `langgraph 1.2.9` | `worker:2267-2289`、`test_run_worker_delta_resume.py:204` |
|
||
| `#4189` 第 3 项 | DeerFlow 设计讨论 | 检查点行的保留与删除;哪些内容可以被删除 | `backend/docs/checkpoint-retention-contract.md`、`backend/tests/test_checkpoint_retention_contract.py`、`backend/app/gateway/checkpoint_retention.py`;目前没有接入生产触发点 | — | `checkpoint_retention.py:1-40`、`backend/docs/checkpoint-retention-contract.md:31-77` |
|
||
| `#3859`(评审) | DeerFlow 评审参考 | 被中断的标题写入必须递增 `channel_versions["title"]` 并在 `new_versions` 中声明,否则数据库 saver 会丢弃该 blob | `backend/tests/test_run_worker_rollback.py:3501-3504` 固定了这一形状 | — | `backend/tests/test_run_worker_rollback.py:3501-3504` |
|
||
|
||
补丁的内容与依赖下限的执行方式见[运维检查点](/docs/harness/checkpoints/operations),每个缺陷对用户表现出的症状见[故障排查](/docs/harness/checkpoints/troubleshooting)。
|
||
|
||
## 变更记录(2026 年 6 月至 9 月)
|
||
|
||
变更记录条目涉及双模检查点存储的 PR。`## [未发布]` 下的英文条目累积发往里程碑 2.1.0;`## [2.0.0]` 下的条目来自 2026-06-15 的发布。「变更记录小节」列给出该条目所在的小节名。
|
||
|
||
| PR | 变更记录小节 | 变更内容 |
|
||
| --- | --- | --- |
|
||
| [#4516](https://github.com/bytedance/deer-flow/pull/4516) | `### ⚠ 不兼容变更(Breaking Changes)` | 配置键从 `database.checkpoint_delta_snapshot_frequency` 迁移到 `database.checkpoint_delta.snapshot_frequency`,默认值从 `1000` 改为 `10`。旧顶层键仍被接受并给出弃用警告,显式设置的嵌套键胜出。delta 模式下快照频率提高 100 倍;要保留原节奏请显式设置 `…snapshot_frequency: 1000`。 |
|
||
| [#4292](https://github.com/bytedance/deer-flow/pull/4292) | `### 新增` | 基于 LangGraph `DeltaChannel` 的双模检查点存储,把长研究/编码任务的线程存储从 O(N²) 降到近线性。 |
|
||
| [#4638](https://github.com/bytedance/deer-flow/pull/4638) | `### 新增` | delta 模式的检查点历史缓存(memory/redis),用 O(1) 增量组合,通过 `database.checkpoint_cache` 配置。 |
|
||
| [#4437](https://github.com/bytedance/deer-flow/pull/4437)、[#4460](https://github.com/bytedance/deer-flow/pull/4460)、[#4468](https://github.com/bytedance/deer-flow/pull/4468) | `### 修复` | 让检查点写入与活跃 run 串行化、线性化 delta 模式的检查点恢复,并接受 SDK 默认的 `stream_resumable=false` 以避免恢复竞态。 |
|
||
| [#4383](https://github.com/bytedance/deer-flow/pull/4383) | `### 修复` | **checkpoint:** 把写入空通道的首个 `Overwrite` 解包。 |
|
||
| [#5255](https://github.com/bytedance/deer-flow/pull/5255) | `### 修复` | 检查点保留契约测试套件:为 full 与 delta 两种 schema 建立按步的增长基线,删除分支祖先会以 `CheckpointLineageError` 明确失败,pending writes 被视为保留状态。 |
|
||
| [#4140](https://github.com/bytedance/deer-flow/pull/4140) | `### 安全` | **storage:** 不再把 base64 图片数据持久化进检查点状态。属于相邻改动,不针对模式。 |
|
||
| [#5734](https://github.com/bytedance/deer-flow/pull/5734) | `### 内部改进` | 将 `langgraph-checkpoint` 提升到 `>=4.2.0,<5.0`、`langgraph-checkpoint-postgres` 提升到 `>=3.1.2,<3.2`,并删除 `InMemorySaver` 的 delta 历史补丁:上游 4.2.0 修复了 full→delta 后首次写入丢失(`langgraph#8526`),postgres 也能定位普通值种子(`#8535`)。迁移契约测试仍是门禁。 |
|
||
| [#4395](https://github.com/bytedance/deer-flow/pull/4395) | `### 内部改进` | **bench:** 新增隔离的检查点通道模式基准,在延迟、存储与重放指标上比较 `full` 与 `delta`。 |
|
||
| [#5051](https://github.com/bytedance/deer-flow/pull/5051) | `### 内部改进` | **bench:** 在检查点基准中与 memory、SQLite 一并测量 Postgres 的检查点/块/写入存储增长。 |
|
||
| [#2582](https://github.com/bytedance/deer-flow/pull/2582) | `## [2.0.0]` → `### 修复` → `#### 运行时、网关与持久化` | 回滚恢复检查点现在会取代更新的检查点。 |
|
||
| [#3559](https://github.com/bytedance/deer-flow/pull/3559) | `## [2.0.0]` → `### 修复` | **subagent:** 让子 Agent 与父 run 的 checkpointer 隔离。 |
|
||
|
||
本列表没有任何条目改动过模式默认值,它一直是 `full`。
|
||
|
||
`#4292` 一行是对已发布 changelog 条目的逐字引用。其中「近线性」的说法描述的是单步表示;保留的存储总量仍然叠加到 O(N²/K + N)(K 为 `snapshot_frequency`),因为每一份周期性快照都保存着当时整份增长中的列表——见[检查点与增量通道](/docs/harness/checkpoints)。
|