2952 Commits

Author SHA1 Message Date
hetaoBackend
9f03fa7563 fix: close upload upgrade compatibility gaps 2026-08-06 23:52:07 +08:00
hetaoBackend
ec7e5e5db1 fix: preserve upload edge-case compatibility 2026-08-06 23:26:24 +08:00
hetaoBackend
297bee0a70 fix: prevent upload lease deadlocks 2026-08-06 23:04:37 +08:00
hetaoBackend
a8f00f30fd fix: close remaining upload lifecycle races 2026-08-06 22:36:37 +08:00
hetaoBackend
96ba5dccd0 fix: coordinate upload aliases at commit 2026-08-06 22:14:24 +08:00
hetaoBackend
783920f75a fix: close upload rollback gaps 2026-08-06 22:01:42 +08:00
hetaoBackend
0082631e23 fix: synchronize every upload ingress 2026-08-06 21:44:08 +08:00
hetaoBackend
cfe2b75588 fix: close upload review gaps 2026-08-06 21:19:43 +08:00
hetaoBackend
d38063b49c fix: enforce local removal mappings 2026-08-06 21:04:22 +08:00
hetaoBackend
e75981a873 docs: clarify upload sandbox boundaries 2026-08-06 20:55:36 +08:00
hetaoBackend
6416c2787a fix: harden upload lifecycle rollback 2026-08-06 20:55:31 +08:00
hetaoBackend
a540ff46a9 docs: document upload generation leases 2026-08-06 20:24:02 +08:00
hetaoBackend
83e908bfe3 fix: mount upload conversions read-only 2026-08-06 20:22:10 +08:00
hetaoBackend
920dc247d2 fix: retain upload leases through adapter sync 2026-08-06 20:19:13 +08:00
hetaoBackend
cadb3a444a fix: retain upload leases through gateway sync 2026-08-06 20:14:23 +08:00
hetaoBackend
797dc97931 fix: bind conversions to upload generations 2026-08-06 20:07:18 +08:00
hetaoBackend
01c5c2099f fix: lease published upload generations 2026-08-06 20:03:06 +08:00
hetaoBackend
3aff322f23 docs: plan upload review remediation 2026-08-06 19:57:55 +08:00
hetaoBackend
9052c1633c docs: design upload review remediation 2026-08-06 19:16:36 +08:00
hetaoBackend
1eed820213 docs: record audited upload adapters 2026-08-06 10:42:34 +08:00
hetaoBackend
d170c7e088 fix: harden remaining inbound upload paths 2026-08-06 10:42:34 +08:00
hetaoBackend
e818efc9e8 fix: preserve missing-upload delete contract 2026-08-06 10:34:43 +08:00
hetaoBackend
8a3cb391f6 docs: define collision-safe upload layout 2026-08-06 10:27:19 +08:00
hetaoBackend
d1f41a9d57 fix: make Feishu attachments collision-safe 2026-08-06 10:24:33 +08:00
hetaoBackend
a0d26e446c fix: make inbound attachments collision-safe 2026-08-06 10:22:27 +08:00
hetaoBackend
0d06a37773 fix: preserve embedded client uploads 2026-08-06 10:18:37 +08:00
hetaoBackend
09d3fc04cd fix: make gateway uploads collision-safe 2026-08-06 10:15:17 +08:00
hetaoBackend
b513921c60 fix: isolate generated upload conversions 2026-08-06 10:11:10 +08:00
hetaoBackend
61ca275131 fix: publish uploads without overwriting 2026-08-06 10:07:38 +08:00
hetaoBackend
3229b7bed4 docs: plan collision-safe upload fix 2026-08-06 10:02:49 +08:00
hetaoBackend
6fe3ace6cc docs: design collision-safe uploads 2026-08-06 09:47:53 +08:00
lllyfff
99c926b7bb
fix(mcp): bring-up has no timeout and externalized tool outputs are counted as undelivered artifacts (#4657)
* fix: bound MCP server bring-up timeouts and exclude externalized tool outputs from delivery verification

Two related robustness fixes:

1. MCP server bring-up was unbounded. tool_call_timeout only covered
   session.call_tool(); tool discovery (subprocess spawn + initialize +
   tools/list) and persistent stdio session initialization could hang
   forever, blocking agent construction (and on the Gateway event loop,
   the whole process). Add a per-server session_init_timeout
   (default DEFAULT_MCP_SESSION_INIT_TIMEOUT = 60s, null disables) that
   bounds both discovery and pooled-session initialization. The session
   pool's existing cancellation handling tears down a session stuck
   mid-creation in its own task.

2. ToolOutputBudgetMiddleware externalizes oversized tool outputs into
   outputs/.tool-results/ (configurable tool_output.storage_subdir). The
   workspace-change scanner and run delivery verification counted those
   files as produced artifacts, so any run that externalized a tool output
   without also presenting a real artifact failed with
   "Artifact delivery incomplete". Exclude TOOL_RESULTS_DIRNAME via a
   shared constant (mirroring BROWSER_FRAMES_DIRNAME) and thread the
   configured storage_subdir through snapshot capture so both
   workspace-changes events and delivery verification stay clean.

* review: enforce single-segment tool_output.storage_subdir; document discovery-timeout cleanup

Address review feedback:

1. A custom tool_output.storage_subdir with a path separator (e.g.
   cache/tool-results) silently no-oped the workspace-scanner exclusion:
   os.walk yields one-segment dirnames, so a nested value never matched and
   its files were counted as produced artifacts again. ToolOutputConfig now
   validates storage_subdir as a single directory name (rejects separators,
   .., absolute, empty) with tests, so the exclusion is always sound.

2. The discovery-timeout path now documents why cancellation is safe, mirroring
   the session-init note: discovery runs inside the adapter's nested async
   context managers, and stdio_client's finally terminates the process tree
   (SIGTERM->SIGKILL on POSIX, process-tree on Windows), so a timed-out npx
   subprocess and its children are reaped rather than accumulating.

* review: log session-init timeouts and align API response model default with runtime config

Address second-round review feedback:

1. A session-init timeout raised TimeoutError without any log, unlike the
   discovery timeout which logs a WARNING. Wrap the bounded get_session in a
   try/except that logs the timeout (server name + seconds) and re-raises, so
   operators can diagnose tool-call failures caused by hung MCP sessions.

2. McpServerConfigResponse.session_init_timeout defaulted to None while
   McpServerConfig defaults to 60s: a server created via PUT /api/mcp/config
   without the field was persisted with null (no timeout) while the same
   server created in the config file got 60s. Align the response-model default
   to DEFAULT_MCP_SESSION_INIT_TIMEOUT so API-created and file-created servers
   behave the same; an explicit null still opts out.

* review: narrow the discovery-timeout handler to the bounded wait_for path

The except TimeoutError clause covered both the bounded wait_for branch and
the bare discovery branch. With session_init_timeout opted out (None), a
TimeoutError raised by discovery itself would hit the %.1f format with None:
logging raises TypeError internally, the WARNING is silently dropped, and a
--- Logging error --- traceback goes to stderr.

Narrow the handler to wrap only the wait_for call, where the branch condition
guarantees the timeout value is not None. A discovery-internal TimeoutError on
the opted-out path now falls through to the generic failure handler and is
reported as 'tool discovery failed' with exc_info. Covered by a regression
test that asserts the skip is reported without any broken format.
2026-08-05 08:56:18 +08:00
Daoyuan Li
480a3757ed
fix(frontend): add public case study routes (#4635) 2026-08-05 08:49:14 +08:00
ajayr
d732b90dc3
feat(channels): add Buzz (Nostr) channel connector (#4649)
* feat(channels): add Buzz (Nostr) channel connector

Adds a Buzz (https://github.com/block/buzz) channel so DeerFlow can join a
Nostr-relay workspace as a member: it answers @mentions in channels, replies
to DMs, and streams answers by editing one message in place.

  * app/channels/buzz_nostr.py — pure NIP-01 helpers: canonical event ids,
    BIP-340 signing/verification, chat/edit/auth builders, relay frames.
  * app/channels/buzz.py — BuzzChannel: one NIP-42-authenticated websocket,
    channel discovery (kind 39000) with one subscription per channel, live
    membership tracking (44100/44101), per-channel replay watermarks, and
    replies posted once then edited in place (kind 40003).
  * app/channels/buzz_run_policy.py — same-thread serialization, mirroring
    the Feishu precedent.

Inbound is gated in order: signature verification, self-drop, /connect
bind-and-return, pubkey allowlist, then mention / DM / mention-free /
thread-follow. Off by default; needs the new optional `buzz` extra
(coincurve, lazily imported), which detect_uv_extras resolves from
channels.buzz.enabled the same way it already handles channels.discord.

Two relay behaviours drove the design and are worth knowing when reviewing:
a global {"kinds":[9]} subscription receives nothing from buzz-relay and a
multi-value "#h" filter receives nothing either, so one REQ per channel is
required; and a single global `since` cursor skips quiet channels, so
watermarks are per channel.

Signed-off-by: Ajay R <ajayr@formbuddy.com>

* fix(channels): only publish assistant messages from the IM stream

`_accumulate_stream_text` decided what streamed `messages-tuple` payloads
become displayable text by rejecting ONLY payloads whose `type` contained
"tool", so it published everything else. DeerFlow writes hidden model
context into the messages channel as ordinary messages -- memory recall and
the rewritten user turn as hidden HumanMessages (DynamicContextMiddleware),
the `<durable_context_data>` block as another (DurableContextMiddleware) --
and LangGraph fans those state writes out on the messages stream, so they
reached every streaming IM channel as the assistant's reply.

Proved live on a Buzz relay: the connector published a `<memory>` fact block
and, in another run, a verbatim echo of the user's own inbound message.
Affects Feishu, Telegram, WeCom and Buzz; worst on Buzz, where each update
is an immutable public Nostr event that a corrective edit cannot unpublish.

Invert the filter to an allowlist of assistant message types. Two new pure
helpers keep it testable:

- `_stream_payload_type` resolves the type from both shapes the function
  already handles: the `model_dump()` shape the gateway emits, and
  LangChain's `to_json()` constructor shape whose own `type` is the literal
  "constructor" and whose class name is the tail of the `id` path.
- `_is_assistant_stream_type` matches "ai"/"assistant" by PREFIX, not
  substring -- ordinary words contain "ai" ("chain", "domain"), and a
  substring test would admit a foreign type name by accident.

The bare-`str` branch is removed: an untyped payload cannot be attributed to
the assistant, nothing in DeerFlow produces one (serialize_messages_tuple
always emits `[message_dict, metadata]`), and a runtime that emitted raw text
deltas would emit hidden context the same way. Per-message-id buffering and
merging are unchanged.

Tests pin both directions, including multi-chunk merging across one message
id, so the allowlist cannot silently kill streaming, plus an end-to-end
`_handle_streaming_chat` test asserting the live payload never reaches an
outbound message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ajay R <ajayr@formbuddy.com>

* chore(helm): bump config_version to 33 in chart values and README

config.example.yaml moved to 33 for the buzz channel block; the chart's
embedded config example and its README copy track it (config_version only
drives the outdated-config warning, per scripts/check_config_version.sh).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ajay R <ajayr@formbuddy.com>

---------

Signed-off-by: Ajay R <ajayr@formbuddy.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:29:22 +08:00
Felix Wang
61c153ff09
feat(tui): add transparent terminal background (#4631)
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-05 08:15:28 +08:00
Baldwinzc
2e9ee329ce
fix(middleware): sanitize bare string blocks in list-form user content (#4668)
InputSanitizationMiddleware's text extraction only collected dict blocks
with type == "text", so a HumanMessage whose content list carried a bare
str item (a shape message_content_to_text treats as text and some IM/SDK
clients send) yielded no text at all — the request passed through
unwrapped and unescaped, letting forged framework tags (<system-reminder>
etc.) reach the model untouched.  The sibling rfind-fallback path in
_process_request already neutralized bare strings individually, and both
ToolResultSanitizationMiddleware and ToolOutputBudgetMiddleware treat
bare strings as text; the extraction helper was the odd one out.

Collect bare string blocks alongside text-block dicts (skipping empty
items, matching message_content_to_text), merging them into the single
sanitized text block on rebuild while interleaved non-text blocks keep
their positions.
2026-08-05 08:06:01 +08:00
dependabot[bot]
9d6633c1e3
build(deps): bump aiohttp from 3.14.1 to 3.14.3 in /backend (#4682)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.14.1 to 3.14.3.
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.14.1...v3.14.3)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-05 07:46:46 +08:00
Xinmin Zeng
74692fec12
fix(tools): resolve presented files with runtime user (#4677) 2026-08-05 07:43:54 +08:00
dependabot[bot]
d8ee55e5ac
build(deps): bump postcss from 8.4.31 to 8.5.25 in /frontend (#4683)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.31 to 8.5.25.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.31...8.5.25)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-05 07:42:00 +08:00
dependabot[bot]
feaf483581
build(deps): bump cryptography from 49.0.0 to 50.0.0 in /backend (#4681)
Bumps [cryptography](https://github.com/pyca/cryptography) from 49.0.0 to 50.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 23:35:25 +08:00
天魔
276481371e
fix(docker): set DEER_FLOW_ROOT for log commands (#4658)
* fix(docker): set DEER_FLOW_ROOT for log commands

* fix(docker): set root before restart

* docs(docker): clarify log command
2026-08-04 22:45:10 +08:00
Nan Gao
1f792d0f4b
feat(extensions): add middleware plugin foundation (#4636)
* feat(extensions): add middleware plugin foundation

* fix(extensions): stop config resolution from masking extension loading

`create_app()` resolved the configured plugin list inside the fail-open
guard around `load_extensions()`. CI has no `config.yaml` (gitignored and
never generated by the workflow), so `get_app_config()` raised
`FileNotFoundError` there and was swallowed as an extension failure --
`load_extensions()` never ran at all, and the four `create_app()` tests in
`test_extension_app_loading.py` passed locally but failed on every runner.

Resolve the plugin list before the guard. Only an absent `config.yaml` is
tolerated, mirroring `_resolve_trace_enabled_for_app_construction()`:
`create_app()` runs at import time, and lifespan still performs strict
config loading before serving. A `config.yaml` that exists but fails to
parse or validate now propagates instead of being reported as an extension
failure -- reporting it as the latter silently dropped a `required: true`
extension rather than failing the boot.

Make the tests config-independent with an autouse `stub_app_config`
fixture, following the existing pattern in `test_gateway_lifespan_shutdown.py`,
and cover both new branches of the config-resolution boundary.

* fix(extensions): bind the run's extension snapshot through subagent delegation

The lead-agent path resolves one immutable loaded-extension snapshot per run
and binds it through task-store allocation and graph construction, but the
subagent path re-read the process-wide singleton at execution time. In
production both are the same object, yet a `set_loaded_extensions()` between
the lead run's start and a subagent's execution (test teardown, a future
hot-reload path) would let one run mix two extension generations — exactly what
the documented invariant exists to prevent.

The graph-build binding is a ContextVar scoped to synchronous construction, so
it has already exited by the time a tool delegates; the snapshot has to travel
through runtime context instead. The run worker publishes it under the
host-internal `EXTENSION_SNAPSHOT_CONTEXT_KEY` (written after the caller merge,
popped when the run has none, so a caller-supplied value is never
authoritative), `task_tool` reads it back through the type-checking
`resolve_run_extensions()`, and `SubagentExecutor` binds it at construction.

Callers outside the Gateway run path — embedded `DeerFlowClient`, standalone
LangGraph Server — install no snapshot and keep the existing
`get_loaded_extensions()` fallback.

* refactor(extensions): defer the ordering table by call, not by a lying tuple

`CORE_ORDERING_CONSTRAINTS` was a `tuple` subclass that overrode only
`__iter__` and resolved into a class-level `_resolved` side channel. A tuple
cannot populate its own storage after construction, so the instance stayed the
empty tuple it was built as: `len()` was 0, `bool()` was False, `in` was always
False, indexing raised, slicing and `reversed()` came back empty, and it
compared unequal to the plain tuples tests substitute for it — all while
iteration yielded the real constraints. Only `assert_ordering` consumed it, and
only by iterating, so the split went unnoticed.

The sibling `_AnchorTable(dict)` uses the same idea soundly because dict is
mutable: `self.update()` fills the real storage, making every inherited
operation correct. That trick does not survive the port to an immutable type.

Replace it with `core_ordering_constraints()`, matching how `stack.py` defers
the same kind of table via `_anchors()`. The deferral is kept — it is about
dependency direction, not just cycles: `extensions/` is the layer the
middleware layer calls into, so a module-scope `agents.middlewares` import here
points the dependency backwards and closes a cycle as soon as any middleware
imports something under `extensions/` at module level. Resolution stays at
`assert_ordering` time, which already runs inside the middleware builder.

Tests pin both halves: the returned value is a plain tuple whose len/bool/
membership/indexing/reversal/equality agree with iteration, and a subprocess
probe asserts importing `extensions.ordering` does not load the middleware
layer while calling the function does.
2026-08-04 22:33:26 +08:00
Vanzeren
bec6277930
fix(frontend): restore user message above same-run steps after mid-run reload (#4660)
* fix(frontend): restore user message above same-run steps after mid-run reload

After a mid-run page reload, replayed messages-tuple steps can reach the
merged list before the turn's human message (the retained replay buffer
may have dropped it), and the live-only human is woven in before the next
shared history anchor. The steps panel then renders above the user
message it belongs to and the per-turn token row splits one run in two.

restoreLocalTurnMessageOrder only covers the locally submitted turn, so
add restoreReconnectedTurnMessageOrder for the reconnected path: a
visible AI/tool step above the last visible human is provably misplaced
when the same run also appears below the human (canonical history is
seq-sorted), or when the step is run_id-less (live-only) and a
run_id-less step follows the human. Pagination orphans from older turns
(#4399) and resent turns after an interrupted run fail both checks and
keep their position.

* fix(frontend): never move a completed turn's terminal answer below the next human

Branch-seeded threads and mocked feeds can share one run_id across turns,
so the same-run sandwich check alone misfired on completed multi-turn
history: a previous turn's final answer was pulled below the next user
message, stripping the turn of its branchable assistant group (e2e
branch-thread.spec.ts).

Only steps after the last terminal assistant answer (visible content, no
tool calls) in the segment are now candidates for reordering. A
still-streaming text step can look like a terminal answer until its tool
call arrives (#4304); it then stays above the human until canonical
history heals the order — an accepted transient, far safer than moving a
completed answer.
2026-08-04 15:07:59 +08:00
Xinmin Zeng
11bb8ddcd9
fix(cli): allow overriding agent recursion limit (#4615) 2026-08-04 08:33:10 +08:00
therenansimoes
bf2cb19ce7
test(skillscan): document known instance-client false negatives from #4296 (#4644)
The instance-client signal is a one-level lexical-scope analysis, and issue #4296
enumerates the cases it deliberately does not report. Four of them were only
described in prose: a handle reached through a container item, a factory return, a
locally aliased constructor, or a dynamic getattr, plus sinks invoked as anything
other than name.method(...).

Pin them in test_python_declared_false_negatives_stay_unreported alongside the cases
already covered, so each is asserted against the runtime oracle -- the client really
is called and the scanner really is silent -- rather than assumed. Re-widening or
narrowing the model now has to change this test.

No behaviour change.
2026-08-03 17:17:01 +08:00
therenansimoes
8659fca8fd
fix(tests): isolate custom agent test from local config.yaml (#4645) 2026-08-03 08:28:15 +08:00
Baldwinzc
91d7f4e4fa
fix(frontend): keep inline-code <think> pairs in content and restore copy for reasoning-only turns (#4647)
Two related fixes in the reasoning extraction path of
core/messages/utils.ts:

1. splitInlineReasoning's first pass stripped every closed
   <think>...</think> pair unconditionally, so a message that discusses
   the tag literally in markdown inline code (e.g. "Wrap your reasoning
   in `<think>...</think>`") had its code span hollowed out and the
   inner text shipped to the Reasoning panel. The streaming pass already
   guards backtick-adjacent openers; apply the same guard to the
   closed-pair pass so both passes agree on what counts as literal tag
   talk.

2. getAssistantTurnCopyData fell back to reasoning via
   `content ?? reasoning`, but extractContentFromMessage never returns
   null, so the fallback was dead code and a reasoning-only turn (e.g.
   stopped mid-thinking) rendered no copy button at all - inconsistent
   with getMessageCopyData, which does copy reasoning in that case. Use
   the same empty-string check it uses.
2026-08-03 08:27:23 +08:00
Aari
da282811b8
feat(frontend): reopen the skill list after a skill is selected (#4639)
* feat(frontend): reopen the skill list after a skill is selected

Selecting a skill closed the composer's skill list for good: `/` no longer
reopened it, so a skill could not be looked up or swapped without deleting
the chip first.

The list now reopens from the editable text beside the chip, and picking an
entry swaps the chip rather than stacking a second activation, since the wire
format carries exactly one leading /skill. Builtin commands are withheld in
that state because they own the whole composer line, and Enter navigates the
list before submitting except while an IME is composing.

The trigger is unchanged: a slash still opens the list only at the start of
the input.

* fix(frontend): keep builtin names reserved in the reopened skill list

Withholding the builtin list from getMatchingSkillSuggestions in chip mode
also disabled the reserved-name filter it drives, so a custom skill named
after a builtin command became selectable there. Nothing rejects such a name
at install time, and submitting the resulting chip runs the command instead
of the skill.

Pass the builtin list as before and drop the builtin entries from the result
instead. The new regression covers both sides of the reservation, and the
reopen test now waits for the list before pressing Enter.

* fix(frontend): hide skills the slash parsers refuse from the picker

The composer picker reserved only the two builtin command names, while both
slash parsers refuse the seven names in the shared contract. A skill named
bootstrap, help, memory, models, new or status was therefore offered, could
be selected into a chip, and submitted — and then activated nothing, because
parse_slash_skill_reference drops the name on the way in. The turn reached
the model as literal text with no skill loaded and no error anywhere.

Reserve the contract names alongside the builtin ones, so the picker cannot
offer what the parsers will not honour.
2026-08-02 22:58:15 +08:00
Ryker_Feng
78f7a5f7c0
fix(frontend): sort chats page by pinned state (#4643) 2026-08-02 22:56:18 +08:00
Vanzeren
c8cf1bf2fb
feat(checkpoint): checkpoint history cache (#4638)
* feat(checkpoint-cache): delta-mode checkpoint history cache with recursive compose

Read-only, invalidation-free cache for LangGraph delta-channel history
({writes, seed}) at the get_delta_channel_history choke point:

- database.checkpoint_cache config (memory|redis; max_entries 0=disabled;
  redis bounded by TTL, Gateway/async only)
- memory LRU backend (copy-on-read, zero-serde hit path) and redis backend
  (lazy import, degrades to all-miss on outage)
- CachedHistorySaver: recursive composition from the nearest warm ancestor
  (depth budget 8), caching each level; depth-0 cold chains delegate one
  inner fast-path walk. Entries keyed by immutable
  (db, thread, ns, checkpoint_id, channel) — no invalidation, coherent
  across workers
- provider wiring: wraps in delta mode only (async + sync), full mode
  untouched; sync path is memory-only
- bench opt-in: DEERFLOW_CHECKPOINT_BENCH_HISTORY_CACHE=1

sqlite bench (500 updates, payload 2KB): write phase 2.28x at f=250,
1.32x at f=10; one delegated walk per thread cold start.

* chore(config): bump config_version to 32 for database.checkpoint_cache

The checkpoint history cache feature added the database.checkpoint_cache
section to config.example.yaml; bump the schema version so existing
deployments get the outdated-config warning and can run make config-upgrade.

* chore(helm): bump config_version to 32 in chart values and README

* fix(checkpoint-cache): purge thread history entries on delete paths

Addresses review on #4638: delete_thread/prune removed source-of-truth
checkpoints but left the thread's materialized history payloads in the
cache (memory: until LRU eviction; redis: until TTL, default 1 day) — a
data-lifecycle gap for tenant offboarding / GDPR-style erasure.

- Cache contract gains thread-scoped adelete_thread/delete_thread
  (lifecycle purge, not invalidation; entries remain immutable)
- Memory backend: stem scan over the LRU map; redis: SCAN MATCH + UNLINK,
  outage degrades to TTL-bounded retention without raising
- CachedHistorySaver purges on delete_thread/adelete_thread and
  prune/aprune (prune rewrites chains, so pre-prune histories must go);
  delete_for_runs stays delegation-only (run->thread mapping unavailable,
  no in-tree callers), documented in code
- ttl_seconds description documents the residual-retention window
- Tests: thread-scoped purge on both backends, saver-level delete/prune
  purge, prefix-safety (t1 vs t10), redis outage degradation, and the
  pinned no-purge behavior of delete_for_runs

* fix(checkpoint-cache): stable db identity, prefix-aware sync singleton, explicit zero TTL

Addresses Copilot review on #4638:

- checkpoint_cache_db_hash now hashes the credential-free postgres
  identity (host:port/database + schema): credential rotation no longer
  changes the cache namespace (cold cache + orphaned keys until TTL).
  Unparseable URLs fall back to the raw string.
- The sync-path memory cache singleton is also keyed by its key_prefix:
  a namespace change (db identity change or operator override) recreates
  the cache instead of leaving stale-prefix entries unreachable and
  unpurgeable.
- ttl_seconds=0 is now an explicit, documented opt-out of redis expiry
  (SET without EX; redis maxmemory policy only) instead of a silent
  'ttl_seconds or None' coercion.

Tests: credential-rotation hash stability, unparseable-URL fallback,
prefix-change singleton recreation, same-prefix singleton reuse, and
zero-TTL wire behavior (ex=None).

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-02 22:25:02 +08:00