* fix(gateway): gate declared multi-instance deployments and require the redis stream bridge
GATEWAY_WORKERS / WEB_CONCURRENCY only count the uvicorn workers of one
process tree, so a Kubernetes Deployment with replicas > 1 (one worker
per Pod) passed every multi-worker startup check while each Pod's
startup orphan reconciliation wrote the other Pods' lease-less runs off
as crashed on every rolling update.
Add `deployment.multi_instance` as an explicit declaration that runs the
same gate, plus a DEER_FLOW_MULTI_INSTANCE env override so deploy tooling
can set the declaration from its replica count without editing
config.yaml (wiring the Helm chart to export it is a separate change).
Both the worker count and the declaration now also require a Redis
stream bridge and refuse an explicit memory sandbox ownership store,
since the memory bridge and ownership store are process-local. The
agent-storage divergence warning, the inbound webhook dedupe warning and
the WeChat QR-login guard honor the same declaration.
* docs(changelog): link the multi-instance gate entry to #6328
* fix(ci): bump chart config_version to 53 and keep AGENTS.md under the size limits
config.example.yaml moved to config_version 53 for the new deployment
section, so the Helm chart's embedded config and its README example
follow. The multi-instance notes in backend/AGENTS.md and
backend/app/gateway/AGENTS.md shrink to one line each: main already sits
within 55 bytes of the per-file hard limit for the gateway guide and
within 163 bytes of the chain hard limit under backend/, and the full
contract lives in backend/docs/CONFIGURATION.md.
* fix(gateway): name every active knob in the multi-process rollback
When GATEWAY_WORKERS > 1 and a multi-instance declaration are both
active, the refusal offered only "Set GATEWAY_WORKERS=1", so an operator
who followed it bounced through a second refusal on
DEER_FLOW_MULTI_INSTANCE / deployment.multi_instance at the next start.
The rollback now withdraws both knobs, and the browser-tools refusal
takes the same path instead of the worker-count-only message.
multi_instance_declaration() returns a frozen MultiInstanceDeclaration
(source, value) with knob/rollback properties instead of a display
string, so deps.py no longer branches on message prose. Tests pin the
combined rollback text and that following it actually clears the gate.
* docs(gateway): keep the gateway AGENTS.md under the size hard limit after merging main
main grew backend/app/gateway/AGENTS.md by 30 bytes, which left it 25 bytes
under the 49152-byte hard limit; the one-sentence multi-instance note from
this branch pushed the merged file to 49164. Shorten that sentence by 18
bytes so scripts/check_agent_guidance.py passes on the merge result.
* ci: re-run checks after test_stop_cancels_a_hung_driver_poll flaked on shard 4
tests/test_mcp_task_service.py::test_stop_cancels_a_hung_driver_poll timed
out once on backend-unit-tests (4) for 2c16206ac. The MCP task service and
its test are identical to main, main's run for cfe2023df passed, and 15
consecutive local runs pass; the test gives service.stop() a 1s budget
while cancellation drain allows 5s, so a loaded runner can exceed it.
Fork contributors cannot re-run a job, hence this empty commit.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* perf(summarization): skip the summarizer LLM call for an input already proven no-op
When the exact rendered summarizer prompt (plus candidate models) previously
produced output byte-identical to the current summary_text, reuse that summary
instead of calling the LLM. Compaction proceeds through the normal path, so
messages, hooks and state transitions are unchanged.
- Only proven no-op inputs are cached (bounded LRU, thread-safe); failed
attempts are never cached and any cache error falls back to the LLM call.
- The middleware:summarize event gains changes.llm_call_skipped.
- Applies to both compact_state and acompact_state.
Builds on the per-call telemetry in the P0 branch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(summarization): keep fallback no-ops retryable
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* docs(scheduler): document tool_enabled, goal notices and goal-evaluation change
Follow-up to #6229:
- CONFIGURATION.md: add scheduler.tool_enabled to the example and notes.
- CONFIGURATION.md, README (en/zh/pt): unmet goal runs send a goal-unmet
notice instead of a completion notice, and three unmet runs add an
auto-pause notice; the docs still said only success/failed notify.
- CHANGELOG (en/zh): the #6229 entry now states the default-on goal
evaluation change for scheduled/webhook/autonomous goal runs and that
evaluator usage counts toward run usage.
* feat(scheduled-tasks): show goal outcomes and end conditions in run history
Follow-up to #6229. The API already returns these fields; the tasks page
did not show them, so a met goal read as plain "Success", an unmet
reason code rendered as a red execution error, and the goal-unmet IM
notice pointed to a workspace showing neither objective nor verdict.
- Task detail shows the per-run goal, max automatic runs and end time
when a conversation-created task has them.
- Goal runs show "Goal met" (or met relying on stated assumptions);
unmet runs show a localized reason plus the host code in neutral
styling. The run whose agent asked to stop the schedule is marked.
- Runs without a goal, and failed runs, render as before.
Read-only and frontend-only: no API change.
* docs(changelog): add #6326 scheduled-task goal outcome entry
* fix(scheduled-tasks): show readable unmet reasons instead of raw codes
Unmet runs now show only the localized reason; the host code moves to the
tooltip so it can still be matched with the IM notice. Codes without a
label are still shown verbatim.
* fix(scheduler): show readable goal reasons instead of internal codes
Task detail now localizes the host-written last_error values from goal and
stop finalization (agent stop, automatic pause, unmet reason codes) under a
matching label, and Chinese copy is tightened. Goal-unmet and auto-pause IM
notices state the reason in words instead of printing the raw code; unknown
codes are still not forwarded.
* docs(changelog): describe readable goal reasons in #6326 entry
* test(scheduler): pin host goal strings with a cross-language contract
Address review on #6326:
- Add contracts/scheduled_goal_notes_contract.json listing the task
last_error strings and unmet reason codes the tasks page and IM notices
translate. A backend test checks finalization constants, blocker and
stand-down codes against it; a frontend test checks every listed value
has a label.
- Move the two last_error strings in finalization.py into constants.
- Frontend reason lookup strips the blocked: prefix like the IM map and
now covers goal_not_met_yet, with en/zh labels.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(harness): detect shell pipe variants with sudo, full paths and other shells
The shell-curl-pipe-shell rule only matched the bare 'curl ... | bash'
spelling. The variants that real install scripts actually ship —
'sudo bash', 'sudo -E bash', '/bin/bash', 'zsh'/'dash'/'fish', and
line-continued pipes — produced no finding at all, so a HIGH-severity
remote-code-execution pattern went unreported (#6310 lists the table).
The pattern now tolerates an optional sudo/doas-style flag prefix, full
interpreter paths, and line continuations, and covers the common shell
names. Non-shell pipes (jq, tee) stay unreported.
Fixes#6310
* test(skillscan): restore lost parametrize decorator in merge resolution
* test(skillscan): fix indentation of the http_host parametrize block
* style: ruff-format the skillscan test file
* fix(skillscan): bound pipe matching and handle real continuations
* fix(skillscan): tolerate continuations between sudo, its flags and the shell
Verified undetected until now: `curl ... | sudo \
bash` and the -E
variant (LF and CRLF). Whitespace runs inside the sudo segment use the same
disjoint (?:\s|\\r?\n)+ shape as the pipe side; value-bearing flags such as
sudo -u root stay intentionally out of scope and are documented in the skills
AGENTS.md note. 17 positive / 6 negative variants verified red-to-green.
* fix(sandbox): keep the exit marker last when appending audit warnings
The medium-risk warning was appended after the bash exit marker and the ToolMessage was rebuilt from four fields. The subagent executor anchors its exit-status regex on the trailing 'Exit Code: N' (and fullmatches 'Command exited with code N'), so a failed command was harvested as success; the rebuild also dropped deerflow_tool_meta, artifact and id. Insert the warning before a trailing marker, leave a bare remote marker untouched, and copy the message instead of rebuilding it.
Document the warning placement in agents/middlewares/AGENTS.md.
* docs(changelog): reference #6307 in the sandbox audit entry
* fix(sandbox): keep a trailing remote exit marker last as well
Match the full sandbox.tools._BASH_EXIT_MARKER_TAIL_RE alternation when
choosing where to insert the medium-risk warning, so output that ends in
`Command exited with code N` after other text keeps the marker last too.
* test(sandbox): import the middleware tail regex inside its test
A module-level import of the new name made the whole file fail to collect
against main, hiding which tests the fix turns green.
Store validated image bytes in the configured content-addressed blob backend and resolve checkpointed refs before local compatibility paths.
Refs: #4189
Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
* fix(persistence): wait out a peer's Postgres bootstrap past command_timeout
The bootstrap advisory lock was taken with a blocking pg_advisory_lock on
the app engine, whose asyncpg command_timeout (30s by default) also bounds
that statement. A second Gateway instance therefore failed startup with
TimeoutError whenever another instance's migration outlasted the timeout.
Poll the non-blocking pg_try_advisory_lock instead: each attempt returns
immediately and stays bounded by command_timeout, while the wait lasts as
long as the holder's migration. The wait is logged once.
* docs(changelog): reference #6306 in the bootstrap lock entry
* docs(persistence): describe polled bootstrap lock in the migrations guide
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(harness): normalize local HTTP host case in skillscan
_LOCAL_HTTP_HOSTS holds lowercase names and every membership test
compared the extracted host verbatim, so an endpoint written with any
uppercase letter (e.g. http://LOCALHOST:8080) was classified as an
external endpoint: non-root deployments failed with EACCES while root
deployments silently wrote plans outside the workspace.
Normalize the host to lowercase in _http_host and in the
network-cleartext/local scan branch so all three call sites agree.
Fixes#6278
* test(skillscan): cover local host case and external endpoint controls
* fix(skillscan): parse IPv6 hosts and detect metadata case variants
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(projects): convert shelf documents outside the SQLite write lock
Lazy conversion ran pymupdf/markitdown inside the BEGIN IMMEDIATE
transaction that serializes the derived/converted.md publish against
trash and purge. On SQLite that lock is database-wide, so every unrelated
writer (run status, thread metadata, the scheduler) waited for the whole
conversion and failed with "database is locked" past busy_timeout (30s).
Conversion now writes into .staging/ outside any transaction. The
document row lock is taken only to revalidate the live row and atomically
rename the output into place (publish_under_live_lock, renamed from
convert_under_live_lock); a row trashed or purged meanwhile discards the
staged output, so nothing is published after purge. A read-only probe
skips converting rows that are already gone, a failed conversion is
still revalidated so a purged row reports content_missing, and the
conversion is drained on cancellation so its staging file is always
removed.
* docs(changelog): reference #6305 in the shelf conversion lock entry
* fix(projects): share one conversion across concurrent first reads
A first-read conversion holds a bounded file-IO pool thread for its
whole duration. With the conversion moved out of the row lock,
concurrent first reads of one document each converted on their own
thread, so a burst of reads could starve unrelated offloaded file work.
Route the conversion through a single-flight keyed by (user, document,
project): the first reader leads and the rest wait on its result
without holding a pool thread. The map holds concurrent.futures.Future
values under a threading.Lock because the read tool also runs on the
isolated subagent loop. Followers wait through asyncio.shield, so a
cancelled follower cannot cancel the shared future, and a leader that
is cancelled or raises hands the work to a follower instead of failing
it.
* fix(skills): stop reporting an env path, shebang or flag as an env dump
* fix(skills): report shell env dumps only at a command position
`shell-env-dump` matched the word `env` anywhere it was not preceded by a path, dot, dash or word character, so ordinary argument text (`echo env`) and comment text (`# env`, `# export -p`) were still reported as environment dumps. Anchor the match to a shell command position -- the start of a line or right after a `;`, `&`, `|`, `(`, `)` or `{` separator, optionally behind `NAME=value` assignment prefixes -- and add regression cases for argument and comment text.
* fix(skills): ignore an env dump inside a comment or heredoc body
`shell-env-dump` matches `env`, `printenv` and `export -p` at a command
position, but the text it sees is still raw shell. A `;` inside a comment
(`# documentation; env is only an example`) and a bare `env` line inside
heredoc data both sit at a command position on paper while the shell runs
neither, so both were reported as environment dumps.
Reduce the text to shell code before matching: a quote-aware pass strips
comments (only a `#` at a word start opens one) and blanks heredoc bodies,
covering `<<-` tab stripping and quoted delimiters, while keeping one output
line per input line so reported line numbers stay correct. Real commands
after a separator (`true; env`) and on the line that opens a heredoc
(`env <<EOF`) still report.
* fix(skills): report env dumps after a keyword and stop masking ${#...}
The command-position anchor for `shell-env-dump` accepted only a line start or a
`;`, `&`, `|`, `(`, `)` or `{` separator, so real dumps were dropped: after a
reserved word that introduces a command (`if true; then env; fi`,
`while true; do printenv; done`), after `exec` (`exec env`) and inside a backtick
substitution (``V=`env` ``). Accept those positions too, with a guard so a path
component (`./then env`) does not arm the anchor, and add the backtick to the
separator set.
A `{` directly before a `#` is not a word start -- bash reads `${#HOME}` as the
length operator and `}#` as part of a word, while a brace group needs the space
of `{ # ...`. The comment heuristic no longer treats `{` and `}` as comment
openers, so `n=${#HOME}; env` keeps its `; env` and is reported again, while
`${env}` (a variable read, not a command) stays silent.
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
* fix(agents): keep malformed todo statuses recoverable
* docs(agents): fit todo guidance within inherited size limit
* docs(agents): clarify todo attribution guidance
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(gateway): scope per-run message and event reads to the run's data identity
start_run stamps run-event rows with the raw trusted owner id, but
GET /runs/{rid}/messages and /runs/{rid}/events called the event store with
user_id=AUTO, which resolves to the internal caller's make_safe_user_id form.
On the database event store, owners whose id contains characters outside
[A-Za-z0-9_-] (IM channel ids such as "feishu:owner-777") therefore got an
empty list. The regenerate source-run scan had the same mismatch and fell
back or returned 409.
Pass _run_scope_user_id() explicitly, as the thread message routes already
do (#5448), and add user_id to RunEventStore.list_messages_by_run() so every
backend accepts it like list_messages()/list_events(). Browser and API
sessions keep their per-user filter.
* fix(gateway): scope workspace-changes and archive reads to the run's data identity
The same raw-owner mismatch reached two more run-scoped routes. On the SQL
stores, GET /runs/{rid}/workspace-changes reported no changes because its
event lookup used user_id=AUTO, and GET /runs/{rid}/artifacts/archive
answered 404 because both its run-row lookup and its run.delivery event
lookup did.
Pass _run_scope_user_id() to both, and let get_workspace_changes_response()
forward an explicit user_id to the event store.
* docs(changelog): reference #6282 in the run-scope read fix entries
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(runtime): keep renewing the lease while a staged terminal run finalizes
With an event store, run_agent stages the terminal status in memory and
commits it to the run store only after the journal flush, delivery receipt,
workspace scan and duration checkpoint. _renew_leases selected runs by local
status, so it stopped renewing as soon as the status was staged. A
finalization longer than lease_seconds + grace_seconds let a peer, or this
worker's own reconciler, claim the still-active row and record the
successful run as an orphan error.
The worker now marks RunRecord.terminal_commit_pending until the deferred
commit is attempted, and lease renewal treats it like the existing
scheduled-goal barrier: keep renewing, release on a confirmed own commit,
and fence when a peer claims the row. A fenced run stops renewing.
* docs(changelog): reference #6263 for the staged terminal lease renewal fix
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(skills): report secret assignments whose key is quoted
The line-oriented secret sweep required the credential name to be followed
directly by ':' or '=', so a JSON config ('{"api_key": "..."}') never matched
while the same binding in YAML, .env, .ini or shell did. Allow one optional
quote between the key and the separator.
* fix(skills): match secret assignments whose key has a separator prefix
The line-oriented sweep anchored the credential word with `\b`, which cannot
land after `_` because `_` is a word character. So `access_token`,
`client_secret` and `MY_API_KEY` — the common spellings in OAuth and
service-account config — matched nothing, and `_SECRET_TOKEN_PATTERNS` does
not backstop JWT- or `GOCSPX-`-style values, so such a package produced no
finding at all.
Guard the word with "not preceded by an alphanumeric" instead: a separator
(`_`, `.`, `-`) may now introduce it, while a run of letters (`tokenizer`,
`secretive`, `credentials_file`) still stays quiet.
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
* fix: include completed child usage in lead budget checks
* test: require completed usage middleware in lead budget regressions
* docs: fit middleware guidance within the size budget
Condense the subagent, loop, and token budget guard descriptions while retaining their limits, lifecycle rules, and completed child usage ordering.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
is_eval_fixture_path returned on the first 'evals' segment, so a path whose
'evals/fixtures/' came after a plain 'evals' segment read as a non-fixture.
Move the 'fixtures' test into the loop condition so every segment is checked.
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
* fix(gateway): read uvicorn's WEB_CONCURRENCY in the remaining worker-count readers
* fix(gateway): name the worker spelling an operator can act on
The browser refusal and the dedupe warning both took the count but printed
GATEWAY_WORKERS, so on the launches that pass uvicorn no --workers the advice
silences the gate while the extra processes keep starting. Resolve which
variable set the count and print that, and let an unparsable GATEWAY_WORKERS
fall through to WEB_CONCURRENCY instead of reporting a single worker.
Also stop the comments from claiming the readers are equivalent: they share
the two spellings and the blank-means-unset rule, but channel_connections.py
turns an unparsable value into 0 and refuses while these stay inert.
* docs: state the single-worker requirement under both spellings
The browser tool group and the inbound dedupe warning now refuse or warn on
WEB_CONCURRENCY as well, so guidance that names only GATEWAY_WORKERS=1 is
incomplete for the shipped Dockerfile and serve.sh launches, where uvicorn
takes the process count from WEB_CONCURRENCY.
* test(features): isolate the browser capability surface from the shell's worker count
browser_capability() asks for the Gateway worker count, which this change makes
readable from WEB_CONCURRENCY, so an exported count in the invoking shell turned
test_features_reports_browser_control_enabled_when_configured_and_runtime_available
into a failure that has nothing to do with the router.
* feat(runtime): add P0 per-LLM-call telemetry and summarization no-op counter
Measurement only; no runtime behavior changes.
- RunJournal enriches llm.ai.response / llm.error metadata with
langchain_run_id, caller_category, provider, model, stop_reason, status,
token counts and rendered request size (chars). Unavailable values are None.
- Summarization middleware emits a middleware:summarize event per summarizer
call and counts outputs identical to the prior summary_text. The call is
never skipped.
- All telemetry fails soft.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(runtime): register summarize middleware tag, document telemetry contract
- Add MIDDLEWARE_SUMMARIZE_TAG to the middleware event catalog and the
contract's known_tags; emit the middleware:summarize event with it.
- Document the optional per-call telemetry metadata on llm.ai.response and
llm.error in contracts/run_event_stream_contract.json.
- Measure request size by walking string leaves instead of json.dumps on the
event-loop thread.
- Initialise summarization call/no-op counters in __init__.
- Move RunJournal telemetry docs to the runtime guide.
- Add tests: llm.error without a start, replayed on_llm_end, and a DB-store
metadata round trip.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(runtime): refresh token counters on usage replay
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
#6229 gave the scheduler its own integer knobs, but they accept YAML
booleans: recursion_limit: true loads as 1, so every scheduler-launched
run dies at its first LangGraph super-step - the same hole the
app-config guard already closes for Gateway runs (#6171), now on the
same-named field. max_concurrent_runs: true serializes the scheduler
and poll_interval_seconds: true polls five times a second.
Mirror the app-config boolean guard: a before-validator rejects booleans
with a field-specific error across all six integer fields, keeping
integers and numeric strings as-is. lease_seconds and
queue_timeout_seconds already reject true via their higher ge floors,
but with a misleading "should be greater than or equal to" message -
the guard gives every field the same boolean-specific error.
Signed-off-by: yetuge <2219677952@qq.com>
YAML booleans coerce into the ge=1 integer backstops that bound every
subagent and ACP invocation: timeout_seconds: true became 1 (the executor's
future.result(timeout=...) then killed every subagent run after one second)
and max_turns: true became 1 (the LangGraph recursion budget collapsed to a
single super-step). The ACP agent timeout mirrors subagents.timeout_seconds
and shared the hole. false was already rejected by ge=1, so only true
slipped through - silently. Mirror the app-config boolean guard: reject
bools before coercion in SubagentOverrideConfig, CustomSubagentConfig and
SubagentsAppConfig (timeout_seconds/max_turns/max_total_per_run) and
ACPAgentConfig (timeout_seconds), keeping None, integers, and numeric
strings as-is.
Signed-off-by: yetuge <2219677952@qq.com>
* fix(memory): treat facts deleted during unlocked DeerMem scans as removed
load(), reload(), and the full rebuild_index() scan list agent fact
files without the storage locks. A delete committed between the listing
and opening a file raised MemoryStorageCorruption for intact data, which
surfaced as HTTP 500 from the memory API and an empty memory block in
the prompt. A listed fact that no longer exists is now skipped; an entry
that is still present but unreadable is still corruption.
* docs(changelog): reference #6255 for the DeerMem vanished-fact fix
* docs(memory): note lock coverage and heuristic limits of the vanished-fact skip
* fix(skillscan): report secrets bound by unpacking and container literals
The AST walk behind secret-env-assignment claims "precision, never less
coverage", but it only read a single assignment target and a bare string
value, so a credential moved into `host, api_key = endpoint, "..."` or
into `api_key = ["..."]` stopped being reported even though the pre-#5648
line sweep still reported the same bytes.
Pair unpacking targets with values positionally (fixed head and tail only,
so a runtime-sized slice names nothing) and assert on each literal a
container or conditional holds, in source order.
* fix(skillscan): distinguish mapping labels from credentials
---------
Co-authored-by: sxh313 <sxh313@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* refactor(config): converge the boolean guards into one shared helper
Willem-bd's #6171 review follow-up: a tiny shared helper lets the next numeric field get the guard in one line and unifies the messages. Consolidates 14 validator copies across 9 config files into deerflow/config/_boolean_guards.reject_boolean(value, info, kind=...); the bare per-file messages gain their field name prefix, command_timeout keeps its distinct wording.
Signed-off-by: yetuge <2219677952@qq.com>
* refactor(config): converge stream_bridge_config and pin the guard wording
Review follow-ups on #6226: convert the last surviving per-file copy (stream_bridge_config) so deerflow/config is fully converged, make kind required keyword-only so a float field cannot silently inherit integer wording, and pin the unified field-prefixed messages with direct tests.
Signed-off-by: yetuge <2219677952@qq.com>
* test(config): cast the boolean-guard test probe to ValidationInfo
The _info probe was annotated -> object, so a type checker flags every call
site that passes its result to reject_boolean's info: ValidationInfo
parameter (runtime was fine since the helper only reads .field_name).
Return cast(ValidationInfo, _Probe(field_name)) to keep the probe honest
for both.
Signed-off-by: yetuge <2219677952@qq.com>
---------
Signed-off-by: yetuge <2219677952@qq.com>
* fix(memory): sign DeerMem reload before reading the document
reload() read the document before computing its cache signature, so a
write committed in between cached the old document under the new
signature and later load() calls served stale memory until the next
write. Compute the signature first, matching load().
* test(memory): cover markdown storage and agent scope in reload race test
* docs(changelog): reference #6238 for the DeerMem reload fix
* feat(extensions): add host-bound Agent run control
* fix(extensions): preserve internal launches and scope run control
* docs(extensions): use a plugin-local run operation key
* fix(lark): match broker subcommand denylist through separate option values
The opt-in DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS check dropped only
`-`-prefixed tokens and compared the rest from the start, so the value in
`--profile work config show` became the leading positional and a
`config show` rule never matched. Real lark-cli 1.0.65 runs `config show`
for that shape and for `config --profile work show`.
The broker cannot know which options take a value, so a rule now matches
when its tokens appear in order among the non-flag tokens. A contiguous
match would still miss a value placed between the path tokens. Argument
values that spell a denied path in order are refused too (fail-closed).
* docs(changelog): reference #6212 for the lark broker denylist fix
* docs(lark): call denylist entries command paths, not prefixes
Entries are matched as in-order subsequences now, so the remaining
'prefix' wording in the BrokerConfig comment, the parse_deny_subcommands
and run_lark_cli docstrings, and the broker README undersold the rule.
Also rewrap the comment and docstring lines this PR had pushed past ~80
columns.