Skip to content

fix(subagents): tear down dispatched child before batch retry - #3

Closed
Zsanz3 wants to merge 82 commits into
mainfrom
cursor/fix-batch-bookkeeping-overlap-adc2
Closed

Zsanz3 wants to merge 82 commits into
mainfrom
cursor/fix-batch-bookkeeping-overlap-adc2

Conversation

@Zsanz3

@Zsanz3 Zsanz3 commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Do not merge this as a replacement for bytedance#5253 — that upstream PR is the review target.

This fork-local draft exists only so the review reply can be recorded. Upstream PR bytedance#5253 already has the same branch.

Thanks for the review at c9851204.

Kept the retain → cancel → wait → finalize order and closed the gaps:

  • _await_dispatched_execution_teardown() can now return False. The wait is bounded to a few lease periods; unconfirmed teardown no longer takes the ordinary immediate-retry path.
  • Teardown is confirmed only by SubagentResult.execution_teardown_event (set after run_with_timeout() returns) or a vanished registry entry — not by CANCELLED/FAILED/completed_at.
  • valid: False / cancel_requested stops the renew/poll loop and returns False instead of finalizing without a held lease.
  • Renewal exceptions are caught, logged with item_id, and treated as the lease-expire path.

Regressions cover lease-lost, unconfirmed teardown, and renewal failure. HEAD is 909a53d547f81f24640d7cf3741ceeffb6ef80e7.

Open in Web Open in Cursor 

PeaceMaker-best and others added 30 commits August 31, 2026 15:22
* feat(skills): install local skill archives

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(skills): enforce upload limits before parsing

* fix(nginx): scope skill upload limit to upload route

* fix(nginx): harden skill upload proxy handling

* fix(skills): improve archive upload feedback

* fix(skills): address upload review polish

---------

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(ci):resolve the skill_review errors

* fix(ci): split skill creator fixes from waiver rollout
* fix(buzz): move seen-event persistence off event loop

* fix(buzz): address seen-event persistence review

* fix(buzz): replace stale scheduled flush tasks

* fix(buzz): harden final seen-event flush

* fix(buzz): make seen-event shutdown retryable

* fix(buzz): quiesce persistence after channel stop

* fix(buzz): drain late events on repeated stop

---------

Co-authored-by: zaoshangduziteng <309590849+zaoshangduziteng@users.noreply.github.com>
…xit (bytedance#5069)

* fix(subagents): clean up background task entry on unexpected poller exit

* fix(subagents): pin deferred cleanup to the persistent subagent loop

The non-terminal fallback scheduled the deferred registry cleanup with
asyncio.create_task on the poller's own loop. Under synchronous tool
invocation the sync wrapper runs the tool coroutine through
asyncio.run(), which cancels caller-loop tasks at teardown, so the
cleanup died before executing and the _background_tasks entry leaked —
the same lifecycle leak the terminal path already fixed.

Schedule the deferred cleaner on the process-owned persistent subagent
loop instead, via the new public executor helper
run_on_isolated_subagent_loop (asyncio.run_coroutine_threadsafe). The
cleaner only touches thread-safe registry helpers, so it is
loop-agnostic. A caller-loop fallback remains for the unreachable case
where the persistent loop cannot be obtained, so scheduling never
raises out of an unwind path that is already handling an error. The
polling-timeout return path, which shares the scheduler, is fixed the
same way.

Tests no longer stub the scheduler: the non-terminal fallback test
drives the real scheduling wrapper on an equivalent long-lived loop and
asserts cleanup runs after asyncio.run() tears the caller loop down,
and run_on_isolated_subagent_loop itself is covered against the real
persistent loop with the caller loop closed underneath.

* fix(subagents): harden interrupted finalization against failing status path

Three edge cases from review on the unexpected-exit unwind:

1. Finalization no longer depends on the failing status accessor.
   _peek_subagent_result distinguishes a gone entry from an unreadable
   one instead of letting the accessor's exception abort the unwind;
   _finalize_interrupted_subagent never raises (so the original poller
   exception is preserved) and attaches the deferred cleaner, whose
   last resort force-removes a persistently unreadable entry via the
   new executor force_cleanup_background_task.

2. The generic-error unwind waits only a short grace period
   (_UNEXPECTED_EXIT_GRACE_SECONDS) instead of the full execution
   timeout before re-raising; the remaining lifecycle stays with the
   deferred cleaner on the persistent subagent loop.

3. The deferred cleaner reports the subagent's final usage (deltas
   since the unwind snapshot included, via final=True bypassing
   usage_reported; the journal dedupes by source_run_id) before
   removing the terminal entry. The report is transferred in a plain
   worker thread so the RunJournal's loop-bound progress flush is
   skipped rather than scheduled on a foreign loop.

* fix(subagents): pin deferred final usage delivery to the parent run loop

_report_deferred_final_usage ran record_external_llm_usage_records in a
to_thread worker, making it the first cross-thread RunJournal writer: the
unlocked accumulators can lose token updates and _tokens_by_model mutations
race get_completion_data() iteration on the parent loop. Capture the parent
loop at unwind time (it is alive in every path that continues the run) and
deliver the final report onto it with call_soon_threadsafe, serialized with
all other journal access; when that loop is already closed (asyncio.run
teardown) the report is dropped on purpose — the run has persisted and
nothing reads the counters back.

The live-loop test exercises the real recorder path (journal captures the
running loop of every call) instead of stubbing _report_subagent_usage, so a
cross-thread report would surface as a wrong-loop entry. Also gates the two
teardown tests on the caller loop actually closing (the deferred cleaner
could otherwise legitimately deliver while that loop is still winding down),
and adds a task_tool-level regression test for execute_async submit failure
leaving no registry residue (rolled back inside execute_async since bytedance#5086).

* docs(subagents): document the reverse loop boundary; observability + test fixes

Extend subagents/AGENTS.md's Isolated-loop callback boundary with the
reverse-direction contract from bytedance#5069: deferred registry cleanup is pinned
to the persistent subagent loop via run_on_isolated_subagent_loop, and the
final usage report is handed back onto the parent run's loop captured at
unwind time — never invoked from the persistent loop or a worker thread,
which would silently reintroduce the journal accumulator/iteration race.

Dropping the final report (closed parent loop) is the one path where a
subagent's tail usage goes permanently unaccounted, so both drop branches
now log at info with the execution id and the unaccounted record count.

Restore the retention assert in test_deferred_cleanup_task_retained_and_
survives_gc: the bounded wait observes the production done-callback discard,
the assert (not a manual discard) is what fails if that callback is
deleted.

* fix(subagents): honour grace-wait cancellation and shrink deferred-cleaner captures

Two review follow-ups on the unwind:

- The shared unwind absorbs CancelledError (never-raise contract), so a
  graph-node cancellation landing inside the generic-error grace wait was
  swallowed and the node ended as a failed tool call instead of an
  interrupted run. The generic branch now re-checks task.cancelling() after
  the unwind and re-raises CancelledError; the absorb site documents why the
  cancellation path needs it and where the discrimination lives.

- The deferred cleaner captured the whole run runtime, pinning the parent
  run's journal and event store for up to a full poll budget (~31 min) via
  the strongly-held task handle — worst on the polling-timeout path, where a
  stuck subagent pinned its run's journal for a second full timeout after
  the tool returned. The recorder is now resolved on the unwind path and is
  the only capture (plus ids and the report loop); a None recorder skips
  reporting entirely.

Both behaviours are regression-tested (red on the previous head, green
after): a cancellation parked inside the grace wait surfaces as
CancelledError, and the runtime is collectable while the cleaner still
polls. The closed-loop drop test now uses a real recorder via
runtime.callbacks so the drop it pins is loop-based, not recorder-absence.
* fix(ci):resolve the skill_review errors

* fix(ci): split skill creator fixes from waiver rollout

* fix(skill-creator): address review findings
…st boundary (bytedance#4839)

* fix(security): sanitize MCP-sourced tool results through the same trust boundary

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* fix(security): sync the trust-boundary docs with tag coverage and pin the untagged branch

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

---------

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
* fix(runtime): clean up terminal run records

* perf(sandbox): bound local path caches

* perf(runtime): release terminal run cycles

* fix(runtime): address terminal cleanup review

* fix(runtime): clean up after end publish failure

* fix(runtime): guard terminal cleanup from cancellation

* fix(runtime): discard fenced journal buffers

* fix(runtime): harden abort and teardown paths
* fix(sandbox): make tool descriptions optional

* fix(sandbox): address optional description review

* test(sandbox): pin optional description contracts
)

* fix(messages): drop legacy <uploaded_files> tag handling (bytedance#4212)

PR bytedance#4174 unified upload-context injection on <current_uploads> (IM and web
both flow through UploadsMiddleware), and bytedance#4632 documented the current
path. This removes the remaining backward-compat parsing of the
pre-bytedance#4174 <uploaded_files> tag, the final cleanup item tracked by the
issue:

- deermem: only <current_uploads> is stripped from human turns before
  memory persistence, and the upload-sentence scrubber drops the legacy
  tag alternative.
- mem0: the mirrored message filter recognises only <current_uploads>.
- InputSanitizationMiddleware: remove the legacy tag from the blocked-tag
  denylist (it existed only because deermem parsed the old tag).
- frontend: stripUploadedFilesTag / stripInternalMarkers /
  parseUploadedFiles and the message-list fallback parse only
  <current_uploads>; demo thread fixtures are migrated to the current tag.

Scope decision: a <uploaded_files> block in pre-bytedance#4174 history is now
treated as ordinary user content (pinned by tests in both layers) instead
of being silently dropped or stripped.

* style: apply prettier formatting to stripUploadedFilesTag

* fix(uploads): keep legacy <uploaded_files> stripping for display/export only

Addresses review feedback on bytedance#4826: removing the legacy tag from the
frontend display layer made pre-bytedance#4174 threads render raw <uploaded_files>
XML (with server-side upload paths) in chat, copy data, and JSON exports.

The backend cleanup stands — memory pipelines and the sanitization denylist
treat only <current_uploads> as an internal marker. The frontend keeps the
legacy spelling in its display/export-only utilities
(stripUploadedFilesTag / INTERNAL_MARKER_TAGS / parseUploadedFiles and the
message-list fallback) so old history renders cleanly without leaking
internal paths, while the memory/sanitization scope-decision tests remain
unchanged.

Frontend tests now pin both spellings: <current_uploads> and legacy
<uploaded_files> are stripped from copy data, markdown leak-stripping, and
JSON exports.

* docs(ui): record accepted display-spoof tradeoff for legacy upload tag

Review note (willem-bd): since <uploaded_files> is off the sanitization
denylist, a live user can type the legacy spelling and fabricate file
chips / hide their own message text in display. Display-only and
self-inflicted with no backend semantics, so it is accepted for now;
documented at both the message-list fallback and stripUploadedFilesTag.
Age-gating the legacy spelling remains a possible follow-up.

---------

Co-authored-by: betterkite <313258397+betterkite@users.noreply.github.com>
* fix(runs): reject cancel actions on GET stream joins

stream_existing_run is registered for both GET and POST, and its
?action=interrupt|rollback branch cancels the run. The CSRF middleware
exempts GET, so a session-authenticated browser could be forced
cross-site (img/script/top-level navigation) into
GET /api/threads/{id}/runs/{run_id}/stream?action=interrupt|rollback —
a state-changing GET that bypasses the CSRF protection guarding the
POST variant. Introduced with the dual registration in bytedance#1403.

The handler's docstring already documents cancel-then-stream as
POST-only (the LangGraph SDK's joinStream/useStream stop button uses
POST); enforce it: GET with an action answers 405, action-less GET
joins and POST cancel-then-stream are unchanged.

Regression drives the real router: GET+action is 405 with the run left
running, plain GET join still streams, POST+action still cancels.

* fix(runs): scope the 405 detail to the action requirement

"GET is a read-only stream join" overstates the current main: on a
locally-owned run with the default on_disconnect=cancel, a GET join's
disconnect can still trigger cancellation. That observer-disconnect
vector is closed by bytedance#5041; the detail here should only claim what this
guard enforces.

* fix(runs): harden GET stream action rejection

* fix(runs): align stream schema with method contract

* test(runs): pin GET stream action 405 through the production stack

Review follow-up (defence-in-depth): the GET-action suite drove bare
FastAPI() apps, so nothing pinned that a session-authenticated
cross-site GET reaches the route gate at all once CSRF exempts the
safe method. test_pat_auth.py already assembles the production
middleware order (AuthMiddleware inner, CSRFMiddleware outer), so its
mirror app now registers the real _reject_get_stream_action
dependency on a GET join route.

The new case pins the end-to-end premise: an authenticated GET
?action=interrupt is answered 405 + Allow: POST by the production
route dependency, while the same unauthenticated GET dies at
AuthMiddleware's 401 before any route logic runs.

Validation: focused suites (test_pat_auth, test_stream_get_action,
test_csrf_middleware) — 62 passed; ruff check + format clean; the new
case errors on the pre-fix baseline (guard absent), confirming the
pin.
…tions (RFC bytedance#4651, layer 2) (bytedance#5109)

* feat(harness): deterministic acceptance checklist for subagent delegations (RFC bytedance#4651, layer 2)

PR4 of RFC bytedance#4651: check lead-supplied acceptance_criteria in code when a
subagent completes, so objectively checkable requirements can never be
silently passed by a self-report.

- subagents/acceptance_checks.py: deterministic leaf families —
  file:<path> exists|non-empty and file_written:<path> read through
  read_current_file_content scoped to the shared thread workspace; the
  read uses the sandbox-native virtual path form (the local read
  validator and provider mount tables resolve /mnt/user-data/... paths,
  not host paths); the scope decision canonicalizes with realpath on the
  local sandbox so workspace symlinks cannot escape into uploads; a
  remote provider's "Error: ..." return string is normalized to a
  failed check (provider-typed via is_local_sandbox); a
  UnicodeDecodeError marks a binary deliverable as existing and
  non-empty; out-of-scope paths degrade to UNVERIFIED.
  tests_passed:<command> anchors to a matching recorded bash execution
  with status=success and a test-summary shape; matching is
  shell-structure aware with control-flow attribution (span must end at
  the last segment with provable execution), negating-option values are
  ineligible evidence and a target negated anywhere in the command
  degrades the match, extra flags must be selection-preserving, extra
  positionals widen only after a path-scoped criterion, truncated
  commands degrade via command_truncated, the summary shape is read
  only from output attributable to the matched segment (preceding
  segments provably silent by invocation form), and pass shapes require
  a nonzero passed count. Criterion text is neutralized with
  neutralize_untrusted_tags before storage/rendering. Anything else
  renders UNVERIFIED, never silently passed.
- executor: accumulate bounded bash command/output evidence per streamed
  chunk (merged by tool_call_id, newest-capped) so subagent
  summarization compacting earlier messages cannot erase a recorded
  execution; the recorded status is the actual shell exit status parsed
  from the output's exit marker (signed codes included; the remote
  Command exited with code N form is accepted only as the whole trimmed
  output), falling back to deerflow_tool_meta only when no marker
  exists.
- sandbox providers: e2b/opensandbox/tenki/boxlite append the
  LocalSandbox-style "Exit Code: N" marker on nonzero exit even with
  non-empty output; aio propagates the SDK's structured exit_code on
  both exec paths the same way; local timeouts append Exit Code: 124;
  and _truncate_bash_output always preserves a trailing exit marker
  (signed included) inside its budget, with a 32-char floor raising any
  smaller configured limit, so the actual shell outcome always survives
  in the output text.
- task_tool: run the checklist offloaded (asyncio.to_thread) on the
  completed branch, failure-isolated; stamp the verdict into result
  metadata and render the per-criterion section into the model-visible
  result text.
- status contract: additive subagent_acceptance_verdict transport with
  read-side structural validation.
- delegation ledger: entry carries the verdict and renders a compact
  acceptance segment; gateway strips caller-forged verdicts from both
  ledger entries and message metadata, like the citation verdict.
- blocking-IO anchor pins the offload (teeth proven red->green); leaf
  read errors catch only OSError/SandboxError so unexpected errors reach
  the task-tool-level isolation instead of being mislabeled.

* fix(harness): close acceptance evidence gaps from review (RFC bytedance#4651 PR4)

- negating options: overlap with a matched criterion target is now
  checked by path/nodeid prefix, not exact token equality — excluding a
  sub-path of the criterion's selection (pytest tests --deselect
  tests/unit/test_auth.py) degrades to UNVERIFIED instead of holds
- output attribution: any redirection token in the matched final segment
  makes the recorded tail non-attributable (> / >> / 2> are word
  characters to the parser, so redirection was invisible to the matcher)
- silent-source allowlist narrowed from any *activate suffix to the
  */bin/activate shape
- status_contract docstring: restore the shared-fixture sentence and
  note subagent_acceptance_verdict is deliberately outside the fixture
- executor: update_bash_executions publishes [] (stream carried no
  bash-family calls) instead of collapsing it into None, mirroring
  update_tool_receipts

* fix(harness): close acceptance residual gaps from re-review (RFC bytedance#4651 PR4)

- tests_passed: add error outcomes to the fail shapes — "4 passed, 1 error"
  and pytest's "ERROR <nodeid>" short summary no longer satisfy the pass
  shape when the exit status is swallowed (|| true) or absent; zero-error
  counts stay clean.
- file leaves: bound the deliverable read — a "wc -c" shell size probe
  answers files above 50k bytes without loading ~2x their size, honoring
  the host-bash kill switch and falling back to the full read on any
  non-integer rendering, so verdicts never get less sound.
- executor: record the exit marker text as status_marker on harvested bash
  evidence; the leaf detail now reports the marker actually seen instead of
  asserting a failure indistinguishable from the command's own trailing text.
- extend the blocking-IO anchor to drive the probe branch inside the
  offload; teeth re-verified red->green.

* fix(harness): close acceptance forgery and bound gaps from P2 re-review (RFC bytedance#4651 PR4)

- file leaves: never read unbounded — size is established first (os.stat on
  the validated local host path, so the host-bash-disabled configuration
  needs no shell; a guarded wc -c on remote providers that renders
  missing/unreadable in its own words). Above the 50k cap the leaf answers
  from the size alone, at/below it the full read runs, and an
  unestablishable size degrades to UNVERIFIED instead of an unlimited
  fallback read.
- output attribution: source/. prefixes are never provably silent — a
  crafted */bin/activate path shape says nothing about what the script
  prints, so sourced segments can no longer lend a passing summary.
- executable identity: an explicitly path-spelled criterion now requires
  the same normalized executable path; the basename rule stays only for
  deliberately bare criterion commands.

* fix(harness): run acceptance size probe outside subagent-controlled state (RFC bytedance#4651 PR4)

- remote probe no longer runs in the sandbox's persistent shell: a fresh
  env -i /bin/sh with absolute-path stat/realpath (poisoned functions,
  aliases, PATH, exported functions, IFS, locale cannot steer it), plus a
  marker env routing AIO onto a fresh per-call bash.exec session.
- metadata-only: stat never opens content, so a FIFO deliverable cannot
  block the parent for the provider's idle timeout; non-regular files
  (fifo/dir/symlink) degrade to UNVERIFIED.
- containment canonicalized against the literal mount root: a
  final-component symlink or a swapped parent directory (root included)
  cannot redirect the check outside shared storage; unprovable layouts
  degrade to UNVERIFIED.

* fix(harness): canonicalize probe containment against the canonical mount root (RFC bytedance#4651 PR4)

Literal-root equality made every remote file leaf permanently UNVERIFIED
on e2b and Tenki, which realize /mnt/user-data as a symlink to the home
dir by default (e2b bootstrap 'sudo ln -sfn', Tenki best-effort symlink).
Containment now compares the file's realpath against the mount root's
realpath — exactly what the provider's own read path resolves, so probe
and read-back stay consistent; final-component symlinks stay rejected by
the non-dereferencing stat, and an intermediate dir-link escape under a
sane root still lands ESCAPED. The inner script is a module constant and
the suite now executes the composed probe for real against on-disk
layouts (real dir, symlinked prefix, final symlink, fifo, missing,
dir-link escape), which the canned-output stub could not see.

* fix(harness): close bare-criterion negation and CDPATH summary channels (RFC bytedance#4651 PR4)

- matching: a criterion with no positional selection target (bare pytest,
  make test) stands for the runner's default selection, so ANY negating
  option (--ignore/--deselect/...) makes the recorded run a different
  selection — unprovable. The overlap guard only sees consumed criterion
  tokens, which a bare criterion does not have; scoped criteria keep the
  unrelated-exclusion behavior.
- attribution: cd is no longer blanket-silent — CDPATH makes cd print the
  resolved (subagent-chosen) destination and the pass shapes match as
  substrings, so one mkdir 'all tests passed' plus an export minted a pass
  for any quiet command. A cd argument or CDPATH= value (export or leading
  assignment) carrying any summary shape makes the segment non-silent;
  shape-free cd dir wrappers keep matching.
- docs: _truncate_bash_output states the effective 32-char floor (the
  guarantee previously read as an unconditional max_chars bound).

* fix(harness): close env-assignment and expansion channels in acceptance matching (RFC bytedance#4651 PR4)

Self-audit in the shape of the last review rounds — channels the matcher
classified as accounted-for that can change what runs, narrow the
selection, or lend the summary text:

- env assignments are no longer blanket-stripped: only an allowlist of
  inert display/CI knobs (CI, NO_COLOR, PY_COLORS, ...) may prefix a
  matched span, and a non-allowlisted assignment in any preceding segment
  (pure-assignment or export NAME=) is state pollution — PATH redirects
  the executable, LD_PRELOAD/PYTHONPATH/NODE_OPTIONS inject code,
  PYTEST_ADDOPTS/GOFLAGS/MAKEFILES inject selection-changing inputs,
  BASH_ENV runs arbitrary shell startup. All degrade to unprovable.
- runtime expansions: any span token carrying /$( )/backticks, any
  negating-option value carrying an expansion or glob (unknown excluded
  set), and any extra executed token carrying glob metacharacters
  (crafted option-looking filenames narrow invisibly) are unprovable.
  Criterion-side globs stay self-consistent (literal match).
- cd: an argument carrying a runtime expansion or glob is non-silent
  (unknown destination, unknown print); CDPATH= assignments are now
  handled as state pollution at the match layer, subsuming the
  value-shape special case.

* fix(harness): persistent-shell evidence, exact env sets, option-arity scoping (RFC bytedance#4651 PR4)

- tests_passed: on a persistent-shell provider (new
  Sandbox.persistent_shell_sessions capability, set by AioSandbox) every
  leaf degrades to UNVERIFIED — any earlier call in the shared session
  could have mutated the state the clean-looking run executed in, and
  only a fresh controlled session (RFC section 6 verifier) can prove
  otherwise. The flag is read from the provider registry without
  acquiring a sandbox.
- env assignments: the allowlist is gone — no variable is provably inert
  across repositories (CI/DEBUG are routinely read by tests). The span's
  assignment prefix must equal the criterion's exactly (values included,
  order-insensitive); any assignment or export NAME= in a preceding
  segment is state pollution.
- scoping: positional targets are now read by option arity, so a path
  embedded in an option (--basetemp=/tmp/p, --junitxml=/tmp/r.xml) never
  counts as a selection target and an extra positional after such a
  criterion narrows the default selection it denotes.

* fix(harness): stamp shell provenance at harvest, close export/unset and arity gaps (RFC bytedance#4651 PR4)

* fix(harness): split physical newlines as shell separators in acceptance matching (RFC bytedance#4651 PR4)

* fix(harness): scope cd wrappers to thread data roots, pin accepted boundaries (RFC bytedance#4651 PR4)

* fix(harness): preserve criterion connectors, prove file_written readable, fail-closed shell capability (RFC bytedance#4651 PR4)

* fix(harness): compare only the connector prefix, tolerate trailing criterion semicolons (RFC bytedance#4651 PR4)

* fix(harness): preserve continuation-line operators, keep ./-spelled executable identity (RFC bytedance#4651 PR4)

* fix(harness): render criteria single-line so a multiline criterion cannot inject a forged checklist line (RFC bytedance#4651 PR4)

* fix(harness): reject parent-traversal executable tokens in acceptance matching (RFC bytedance#4651 PR4)

* fix(harness): reject parent-traversal negated values in acceptance matching (RFC bytedance#4651 PR4)
* fix(ci): fix the ci check of gateway AGENTS.md

* fix(ci): fix the ci check of gateway AGENTS.md
…5119)

* refactor(gateway): issue request trace ids unconditionally

The request trace id was gated behind logging.enhance.enabled at every
entry point, so downstream code had to keep asking whether one existed:
a header-provenance flag in its own ContextVar, a precedence resolver,
and three-level carrier fallbacks at each consumer.

Bind one unconditionally instead. TraceMiddleware covers Gateway HTTP;
ensure_trace_context covers the entry points that never touch ASGI --
scheduled occurrences, MCP task notification runs, IM channel messages,
and the embedded client -- each scoped to one unit of work so a
long-lived worker task cannot leak one occurrence's id into the next.
The ContextVar becomes the only source; the response header, runtime
context, run metadata and log records are derived outputs.

Consumers now use ensure_trace_id() or resolve_trace_id(*carriers) and
drop their presence guards. Removed: resolve_deerflow_trace_id, the
header-provenance flag and its three helpers, set/reset_current_trace_id,
is_trace_correlation_enabled and its gateway alias.

BREAKING CHANGE: every Gateway HTTP response now carries X-Trace-Id and
it cannot be turned off; logging.enhance.enabled controls log output
only. Installations on the default enabled: false will start seeing the
header. No config keys were added or removed.

* fix(gateway): stop persisting a caller-supplied trace id on the run record

body.metadata forks two ways: through build_run_config into the live run
config, which the run worker restamps, and through create_or_reject into
the run record that the runs API echoes verbatim. Only the first was
covered, so a client sending metadata.deerflow_trace_id made the most
durable and most visible surface of a run disagree with the X-Trace-Id
and the log lines the same request produced -- a correlation id that
does not match the logs is worse than none.

Stamp the server-issued id once at the trust boundary so both forks
receive it, preserving the caller's own metadata keys. Close the same
gap on config.context, which reaches the runtime context by a separate
path: _build_runtime_context no longer merges server-owned keys from the
caller, and _install_runtime_context assigns rather than setdefaults.

A thread's metadata is no longer seeded with the run-scoped id of
whichever run created it -- one thread spans many runs and as many
trace ids.

Found by driving a real run through the Gateway and reading the run back
from the runs API; every unit test built its metadata by hand and so
could not see it.

* fix(gateway): expose X-Trace-Id to split-origin browser clients

X-Trace-Id is not on the CORS safelist, so a browser client served from
a separate origin could not read it -- and those are exactly the clients
that cannot read the Gateway's logs either, leaving them with nothing to
quote in a bug report. Same-origin nginx deployments were unaffected,
which is why this stayed hidden.

Add it to CORS_EXPOSED_HEADERS beside Content-Location, referencing
TRACE_ID_HEADER rather than repeating the literal.

* fix(gateway): keep X-Trace-Id on unhandled-exception 500s

Starlette's ServerErrorMiddleware sits outside every user middleware and
emits unhandled-exception 500s through the raw send, so those responses
never pass TraceMiddleware's header-writing wrapper. The 500 for a server
bug is exactly the response a user most needs to correlate with a log line,
and it was the one response that shipped without the id.

TraceMiddleware now tracks whether http.response.start has been sent. On an
exception with no response started it emits its own plain 500 carrying the
header, then re-raises: the outer ServerErrorMiddleware sees the response
already started and only re-raises too, so the server's exception logging is
untouched. An exception mid-stream keeps propagating unchanged — a second
response start cannot be sent, and the already-written header stands.

The trace id is printable ASCII by construction (normalize_trace_id /
generate_trace_id), which is what makes the raw latin-1 header encoding
safe.

* fix(gateway): strip the forged trace id from the persisted request echo

The run-record fix stopped a forged metadata.deerflow_trace_id on the
authoritative metadata surface, but the raw request echo still carried one:
create_or_reject persists body.config verbatim as runs.kwargs_json, which
the runs API serves back. A client posting config.context.deerflow_trace_id
therefore still got its forged value stored and echoed on one API surface
while the header, logs, run metadata, and checkpoint all carried the real
id — the id is ignored as input there, so echoing it back only manufactures
disagreement.

Two changes close it. redact_config_secrets — already the shared scrub for
that echo, applied at admission and again at serve time, so historical
records are covered too — now also drops deerflow_trace_id from
config.metadata and config.context. And build_run_config now merges run
metadata onto a copy of the caller's config["metadata"] instead of updating
it in place: the nested values of the request config are reference copies,
so the in-place merge was writing the server-stamped key through into
body.config, contaminating the "what the client sent" record before it was
persisted (and incidentally masking the forged-value echo on the metadata
container).

The regression test posts a forged id through body.metadata,
config.metadata, and config.context at once and reads the kwargs echo back
off the run record, failing if either leak returns.

* docs(harness): record the trace-echo scrub, 500 fallback, and accepted retry divergence

The trace section of the harness AGENTS.md now covers the two fixes that
close the derived-output rule (the kwargs-echo scrub in
redact_config_secrets plus build_run_config's copy merge, and
TraceMiddleware's own 500 for unhandled exceptions), and CHANGELOG gains
their Fixed entries.

It also writes down the one accepted divergence: a crash-recovered
scheduled launch reuses the durable run through its idempotency key, and
start_run returns early on idempotency_reused without restamping — so the
run record keeps the first attempt's deerflow_trace_id while the retry's
own log lines carry the freshly minted id of its ensure_trace_context
binding. The divergence is confined to the crash-recovery window and is
accepted rather than fixed: restamping on reuse would rewrite a persisted
record for a run that already exists, which is worse than two ids that each
correlate their own attempt's logs. Written down so the next reader of the
scheduler recovery path does not diagnose it as a bug.

* docs(config): align the logging.enhance schema note with the unconditional trace id

The config-module AGENTS.md still described logging.enhance as the gate for
the Gateway X-Trace-Id header and Langfuse deerflow_trace_id. That model is
gone: ids are issued unconditionally and this block decides log output only.
Left as-is, the stale wording invites an agent to "restore" a header gate it
believes was lost. Reworded to match the sibling AGENTS.md files and
config.example.yaml, with a pointer to the Request Trace Context section
that owns the full model.

* docs(changelog): link the trace entries to bytedance#5119

The five new entries pointed at the ([#XXXX]) placeholder with no reference
definition, rendering as literal text instead of a link — and RELEASING.md
step 2 relies on those references when the section becomes release notes.
All five now point at bytedance#5119, with the definition appended to the reference
block.

* refactor(harness): rename _stream_without_trace_context to _stream_turn

The name asserted the opposite of what the method now does. It was accurate
while logging.enhance.enabled could route stream() around the trace scope;
with the gate gone it is the only stream implementation left, and it binds
the id itself via ensure_trace_id(). Private, so the rename touches only the
definition and the one stream() call site.

* docs(harness): fit the trace-context guidance inside the AGENTS.md chain budget

The expanded Request Trace Context section pushed the effective AGENTS.md
chain for agents/middlewares to 99,815 bytes, past the 98,304 hard limit
scripts/check_agent_guidance.py enforces in CI (AG002). Compressed the
section from 7,359 to 4592 bytes with no facts removed: the entry-point
table, the derived-output rule and its enforcement points, the accepted
scheduled-retry divergence, the two resolution helpers, the stream()
binding rationale, the log-output-only gate, the CORS listing, the 500
fallback, and the test map all remain.

Sized against the merge, not just the branch: current main grew the same
chain by ~724 bytes, so the check was verified on the merged tree as well
(97,772 bytes; branch tree 97,048).

* fix(gateway): declare content-length on the fallback 500

The pre-response 500 declared content-type but no content-length, leaving
the framing to the ASGI server: chunked on HTTP/1.1, close-delimited on
HTTP/1.0 — the one wire difference from the ServerErrorMiddleware response
it replaces, which sends content-length: 21. The explicit header keeps the
fallback byte-identical to what clients saw before.

* docs(readme): drop the trace-correlation condition from the translations

The zh/ja/fr/ru Langfuse sections still said metadata.deerflow_trace_id
matches X-Trace-Id "when request trace correlation is enabled". The id now
always matches and that condition no longer exists, so each bullet states
the unconditional match and that logging.enhance.enabled only controls
whether the id is printed into logs — the one piece of the feature a user
can still configure.

* test(gateway): pin TraceMiddleware wiring through create_app()

Every X-Trace-Id test exercised a hand-built four-route app, so the real
stack's add_middleware(TraceMiddleware) line was pinned by nothing: deleting
it — or short-circuiting above it — passed CI while silently dropping both
the response header and the ambient id the run-record stamp and enhanced log
records derive from. One case now drives /health through create_app() and
asserts the inbound id round-trips; mutation-checked by removing the wiring
line, which fails exactly this test.

* docs(gateway): note the fallback 500 is CORS-opaque

The pre-response 500 is emitted outside CORSMiddleware — the exception has
already unwound past it — so it carries no Access-Control-Allow-Origin and
a split-origin browser client cannot read the id on this one response,
unchanged from the ServerErrorMiddleware 500 it replaces. Documented on the
class and in the CHANGELOG entry rather than fixed: replicating the origin
allowlist outside CORSMiddleware would let the two policies drift.

* fix(harness): keep abandoned-stream cleanup inside the trace binding

stream() binds the turn's id around each next(inner) and resets it before
yielding, but the finally's inner.close() ran after that binding was gone.
Abandoning the stream therefore drove the inner LangGraph generator's
GeneratorExit/finally path with no trace id — or an unrelated ambient one
from whichever context ran the close — so cancellation and finalization
logs and callbacks did not correlate with the turn they belong to.

inner.close() is now wrapped in a local bind/reset of the same turn id. The
token is set and reset in the same frame, never across a yield, so the
per-step cross-context safety is preserved even when GC closes the
generator from another Context — pinned by the existing copy_context close
test, which now exercises this path. The regression test records the id
from the inner generator's finally and fails without the binding.

* test(harness): teach the worker-trace fake about RunManager.cleanup

Upstream bytedance#5112 (bound gateway memory after terminal runs) added a
run_manager.cleanup(run_id) call to run_agent's finalization, so the
merge-commit CI run failed all five worker-trace-binding tests with
AttributeError on this PR's _FakeRunManager. The fake gains the same no-op
shape as its other methods.

* docs(gateway): bring the gateway AGENTS.md back under its soft budget

Upstream bytedance#5092 grew backend/app/gateway/AGENTS.md to 40,966 bytes, 6 over
the 40,960 soft budget that
test_agent_guidance_check.py::test_repository_guidance_stays_below_soft_budgets_and_avoids_doc_indexes
enforces — its Unit Tests run on main was cancelled by push concurrency, so
main is currently red on that test and every PR merge-run inherits the
failure. Two whitespace/wording trims in the row bytedance#5092 touched (a doubled
space, and "its configured `context_window`" → "its `context_window`")
bring the file to 40,953 with no content change.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(artifacts): download run files as zip

* fix(artifacts): address archive review feedback

* fix(artifacts): gate unavailable archive downloads

* fix(artifacts): verify archive availability

* fix(artifacts): harden archive consistency

* fix(artifacts): reject archive path aliases
…ion and context compaction overlap (bytedance#4696)

* fix(history): stop dropping user messages that fall outside the loaded page window

Two independent paths made a user's own message disappear from a long thread
(bytedance#4666, bytedance#4508, bytedance#4363). Both are reproduced by a real two-round run: once the
thread passes the 50-row `/messages/page` window AND context compaction fires,
the two sources of truth stop overlapping at the head.

1. Middleware-answered tool results never reached the event store. A middleware
   that short-circuits a tool call (e.g. ReadBeforeWriteMiddleware's blocked
   write) returns a user-visible ToolMessage, but LangChain never emits
   `on_tool_end`, so RunJournal never persisted it — the user saw it during the
   run and it vanished on reload. RunJournal already reconciles final-output
   tool messages, but only for an `ask_clarification` allowlist. The allowlist
   is removed; scope stays bounded by the three conditions that actually matter
   (visible, this run's lead agent, not already persisted), so subagent results
   still stay in their own step feed.

2. mergeMessages discarded the checkpoint prefix before the first shared anchor.
   bytedance#4065 correctly established that a summarization-rescued early message must
   not be appended to the tail, and suppressed it instead. That suppression is
   what deletes the message when the first history page no longer reaches back
   to it. It is now woven in before the first shared anchor — the one position
   both the checkpoint and seq-sorted history agree on — so bytedance#4065's invariant
   (never the tail) still holds. A collapsed unloaded gap is recoverable by
   paging; a dropped message is not.

Verified against real captured payloads from the reproducing run: the first user
message returns to the transcript. Its exact position is still approximate —
after compaction the live window carries too few anchors to place it precisely,
which only seq-based ordering can close.

Backend: 10809 passed (baseline 10808; same 15 pre-existing failures in
browser/crawler community tools). Frontend: 986 passed, typecheck + eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(events): look up a persisted message's seq by identity

Groundwork for placing checkpoint messages in the seq-ordered thread feed
(bytedance#4666). A checkpoint carries no seq of its own and loses messages to
summarization, so once the feed's 50-row page window no longer reaches back to a
surviving old message, a client has nothing to place it by. The seq already
exists in run_events keyed by the message id — this exposes it without paging
the whole feed.

`message_identity` is the backend half of the identity rule the frontend applies
in `hooks.ts::messageIdentity`: a ToolMessage is keyed by `tool_call_id`, and
DynamicContextMiddleware's `X` / `X__user` human copies collapse to one identity.
The two halves must stay in sync — a mismatch is silent, degrading placement
rather than raising.

`get_message_seqs` is implemented for all three stores. Misses are absent from
the result rather than an error, so callers degrade to their own placement rule;
the earliest seq wins when one identity resolves to several rows, so a
re-persisted message keeps the position it first occupied. The DB store decodes
rows in Python because `content` is a TEXT column holding a JSON string, not a
JSON column — the identity fields cannot be projected in SQL.

Nothing consumes this yet; no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(runtime): carry each persisted message's feed seq on values frames

Attaches `additional_kwargs.deerflow_seq` to messages in a root `values` frame
that the thread feed already holds, so a client can place a message the
checkpoint kept but its loaded history page window no longer reaches (bytedance#4666).
Nothing is written back to the checkpoint: the seq is added when the frame is
serialized and belongs to that frame only.

Cost is bounded to frames introducing identities the run has not resolved yet.
Messages this run produces are not in the feed while streaming, so they are
looked up once, recorded as misses, and never retried — in a real run the only
frame that pays for a query is the one where compaction brings older messages
back into view. Measured on a reproducing two-round run: 1 lookup across 25
values frames.

The stamper is built once per run rather than per `_stream_once`, or a goal
continuation would discard the resolved seqs. Subgraph frames are not stamped:
a subagent's snapshot is not part of this thread's feed ordering. A lookup
failure logs and leaves the frame unstamped rather than failing it — placement
is an enhancement and clients fall back to their own ordering rule.

Frontend does not read the field yet; no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(gateway): strip the server-owned message seq from untrusted input

`deerflow_seq` is display metadata the Gateway attaches when it serializes a
values frame. A client replaying messages (regenerate / edit-and-rerun) would
otherwise write it into the checkpoint, where it becomes wrong the moment the
thread is forked — a branch re-seeds its feed and reassigns seq (bytedance#4380).

Joins the existing server-owned key set, so it follows the same trusted-internal
rule as the dynamic-context and view-image markers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): place a checkpoint message by its feed seq, not its nearest anchor

Completes bytedance#4666. Weaving a compaction-rescued message before the first shared
anchor keeps it in the transcript, but not in the right place: after compaction
the live window carries too few anchors, and the nearest one can sit deep inside
the loaded page window — measured at row 25 of 50 on a reproducing run, which is
why the first user turn rendered mid-transcript instead of at the head.

Both sides now carry the backend's thread-global seq. `buildVisibleHistoryMessages`
copies each row's `seq` onto the message (same shape as the existing `run_id`),
and the Gateway stamps it onto `values` frame messages it has already persisted.
A live message whose seq is below the loaded window's lower bound is placed ahead
of everything on screen rather than before the nearest anchor. A message with no
seq — still streaming, so not in the feed yet — keeps the weaving path, since the
tail is already its correct position.

Verified against the captured payloads of the reproducing run: the first user
message goes from absent, to bytedance#13 (behind the second question), to #0.

Frontend: 988 passed, typecheck + eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): place a pre-window checkpoint message even when no anchor is shared

Also bytedance#4666. Placing a compaction-rescued message by its feed seq was gated on
reaching a shared anchor, because the split ran inside the anchor walk. When the
loaded page and the live checkpoint share no identity at all, that walk never
runs and the message fell through to `[...canonical, ...live]` — appended after
the entire window, the one arrangement bytedance#4065 proved wrong, with its seq known
the whole time.

That is not a corner case. Open an old, already-summarized conversation and send
a message: the page on screen is the newest rows from before that turn, while
the checkpoint holds the rescued first user turn plus steps of the new run that
are not in the feed yet. On a reproducing run the two sides shared zero anchors
and the user's own first question rendered at row 50 of 50 — the reported
"first message jumps to the bottom".

Split `beforeWindow` out of `live` before walking anchors, walk `liveInWindow`,
and use it for the no-anchor branch as well, so a message routed ahead of the
window is not re-appended at the tail by dedup.

Measured on captured payloads of a reproducing run (real gateway, real
compaction), first user message position:

  no shared anchor:  row 50 -> row 0, seq order monotonic again
  shared anchors:    row 0 -> row 0 (unchanged)
  paged to the top:  row 0 -> row 0 (unchanged)

Regression test verified red-green: reverting the fix fails it with the message
rendered after the window.

Frontend: 989 passed, eslint + tsc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(gateway): stamp the message feed seq on checkpoint reads, not only on stream frames

Completes bytedance#4666. `_MessageSeqStamper` sits on the streaming publish path, so a
client that joins a live run learns where a summarization-rescued turn belongs
while a client that merely opens the conversation does not — and opening is the
common case. `GET /threads/{id}/state` and `POST /threads/{id}/history` returned
the checkpoint with no seq at all, so the merge fell back to the nearest shared
anchor, which after summarization sits deep inside the loaded page.

Reproduced in a browser against a real gateway, on a thread that had already
compacted: the user's first question rendered at row 320 of 389, behind the
newest question instead of at the head. Both reads showed 0 of 13 messages
carrying a seq. That is the reported symptom, still present after the streaming
fix.

Add `stamp_messages_with_seq`, the request-scoped counterpart of the stamper:
everything a checkpoint still holds is already persisted, so one batched lookup
resolves the whole list and there is nothing to retry later. Resolve the store
through `_optional_run_event_store` rather than `get_run_event_store`, because
seq is placement metadata — a deployment without a feed must still be able to
read a thread.

After the fix, on the same thread in the same browser: 13 of 13 messages carry a
seq and the first question renders at the head, ahead of the newest one.

Backend: ruff clean, 326 passed across the touched suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(harness): move the injected-user-id suffix helpers to utils.messages to break an import cycle

message_identity imported strip_injected_user_message_id_suffix from the
dynamic-context middleware, closing a cycle (middleware -> deerflow.runtime
-> worker -> events -> middleware) that only stayed hidden while an earlier
import happened to break it. Define INJECTED_USER_MESSAGE_ID_SUFFIX and the
strip helper in deerflow.utils.messages and re-export them from the
middleware so existing importers keep working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(docs): improve formatting and clarity in AGENTS.md and message-merge.test.ts

* perf(events): stop the seq scan once every wanted identity is resolved

Rows past the last wanted seq can only be re-persisted copies that
already lose the earliest-seq-wins tiebreak, so all three stores now
break out of the scan (and the db store out of its per-row JSON
decoding) once found covers wanted. Matters most for /state and
/history reads of long threads, where this lookup runs with no run
cache and a typically tiny wanted set.

Raised by review on bytedance#4696.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(events): share the seq-stamping expression between the two stampers

The walrus-plus-merge expression was duplicated verbatim between
stamp_messages_with_seq and _MessageSeqStamper.stamp — two counterparts
of one rule where silent divergence is the likely failure mode if only
one side is edited. Both now call attach_message_seq next to
MESSAGE_SEQ_KEY in message_identity.py. The trailing
isinstance(message, Mapping) guard was unreachable (a non-Mapping entry
already got identity = None) and is gone with the extraction.

Raised by review on bytedance#4696.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(events): seq stamping survives launch paths without user context

The db store's get_message_seqs defaults to user_id=AUTO, which raises
when no user is in the contextvar — the first strict-AUTO read ever
called from the worker context. On a launch path that never inherits
the auth context (e.g. a null-owner scheduled task), stamp()'s except
clause swallowed that into a per-frame warning and silently disabled
seq stamping for exactly the background runs that need it.

The stamper now soft-resolves the user id once at build time — the
same rule as the worker's write paths beside it (unset -> no filter)
— and passes it explicitly. jsonl/memory stores gain the same
user_id kwarg the base list_messages contract already carries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(events): SQL-prefilter the message seq lookup's candidate rows

get_message_seqs scanned and JSON-decoded every message row of the
thread: the early exit never fires when a wanted identity is absent
from the feed (a message still streaming, or checkpoint-only), and
/state / /history reads want the newest messages, so the ascending
scan traversed essentially the whole feed — with the content column
carrying full tool outputs, that is heavy I/O plus N JSON parses on
exactly the long threads this lookup exists for.

A LIKE prefilter now keeps that cost in SQL: only rows containing a
wanted raw id as a substring are fetched and decoded. False positives
are re-checked by message_identity; LIKE wildcards are escaped; an id
json.dumps would escape (breaking the verbatim-substring guarantee)
falls the whole set back to the full scan rather than silently
missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agents): sink runtime mechanism docs below the gateway guidance budget

Merging main pushed backend/app/gateway/AGENTS.md past its 40KB soft
budget (main had left 81 bytes of headroom). Per the nearest-file rule,
move the mechanism detail of the message-seq stamping and run-delivery
receipt sections — both owned by runtime/ code — into
packages/harness/deerflow/runtime/AGENTS.md, leaving the gateway file
the REST-surface summary and a pointer. The seq section also documents
the stamper's build-time soft user-id resolution and the db store's SQL
prefilter from the review follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agents): sink durable-MCP task detail below the backend guidance budget

Merging main pushed backend/AGENTS.md past its 24KB module soft budget
(main itself is at 24762 after bytedance#4848 — this branch adds zero net bytes
to the file). Per the nearest-file rule, move the two durable-MCP task
runtime bullets' mechanism detail into
packages/harness/deerflow/mcp/AGENTS.md, leaving summaries and
pointers; this also restores ~2KB of headroom so the next merge does
not trip the same wire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(events): re-ask a message-seq miss once the feed advances

The run-scoped stamper cached lookup misses for the whole run. A message
this run produces reaches a values frame before RunJournal flushes it, so
its first lookup legitimately misses — and the journal persists it moments
later, giving it a feed seq the stamper never asks for again. A long run
that afterwards rolls past the history page and compacts then carries that
message unstamped, back to the approximate anchor placement this stamper
exists to replace (bytedance#4666). A transient store error had the same permanent
effect, since the except clause degrades to an empty result.

A miss is now provisional while a hit stays final: RunJournal counts its
successful event-store writes as `feed_generation`, and the stamper re-asks
a missed identity only once that counter moves. Retrying is therefore
bounded by feed writes rather than by frames — the per-frame query the
run-scoped cache was built to avoid — and a failed lookup costs one
generation instead of the run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…CSS worker leak on macOS (bytedance#5133)

* fix(frontend): default to Webpack over Turbopack in dev to avoid PostCSS worker leak on macOS

On macOS arm64, Turbopack + Next.js 16.2.11 + Tailwind CSS v4 causes an
unbounded spawn of PostCSS evaluator processes that consume high CPU and
memory and never return a response. Webpack is unaffected.

Change the no-override default in getDevBundler() from platform-dependent
Turbopack (all non-Windows) to Webpack. DEER_FLOW_DEV_BUNDLER=turbo
continues to work as an explicit opt-in for local diagnosis.

Fixes bytedance#5132

* docs(frontend): address webpack default review feedback

* docs(frontend): clarify webpack default rationale
* fix(frontend): 同步会话重命名后的标题状态

* chore: 重新触发 PR 自动分流检查

* fix(frontend): address thread title sync review feedback

Signed-off-by: 橘猫 <2622045569@qq.com>

* fix(frontend): fall back when canonical thread title is empty

Signed-off-by: 橘猫 <2622045569@qq.com>

* fix(frontend): fence stale metadata after thread rename

Signed-off-by: 橘猫 <2622045569@qq.com>

* fix(frontend): fence stale thread list responses after rename

Signed-off-by: 橘猫 <2622045569@qq.com>

---------

Signed-off-by: 橘猫 <2622045569@qq.com>
* feat(mcp): manage servers from settings

* fix(mcp): make settings updates targeted

* fix(mcp): reject ambiguous masked array edits

* fix(mcp): honor targeted server field deletions

* fix(mcp): preserve OAuth extension secrets

* fix(mcp): validate config before persistence

* fix(mcp): preserve environment placeholders

* fix(mcp): harden targeted configuration routes

* docs: keep gateway guidance within budget

* fix(mcp): protect per-tool override secrets

* fix(mcp): keep disabled edits structurally safe
…tedance#5136)

* fix(frontend): truncate long subtask card titles to a single line

The subtask card header rendered task.description without any width
constraint; when a provider omits the optional description, the full
task prompt becomes the title and overflows the card.

Wrap the title in a truncating span (full text remains available via
the title tooltip and the expanded card body), give the step min-w-0
flex-1, and pin the status cluster with shrink-0 so overflow resolves
at the title.

Add an e2e test asserting a long prompt renders with the truncate
class, a real ellipsis (scrollWidth > clientWidth), and single-line
height.

* fix(frontend): keep subtask card status cluster shrinkable on narrow viewports

The shrink-0 status cluster could not shrink below its max-content (model
label + usage + status pill, up to ~456px with a long tool-call
description), so on narrow viewports it overflowed the header row while the
title collapsed to zero. Drop shrink-0 and add min-w-0 to both the cluster
and the pill (the pill's min-content is the status text's longest
unbreakable word, so one min-w-0 was not enough), and floor the title at
min-w-24 so it stays visible.

Also extend the e2e spec per review: an in_progress shimmer truncation test
(held-open SSE stream keeps the card running), a 375px no-overflow
assertion for both the resting and running card, and a pixel-budget
single-line check instead of parseFloat(lineHeight) which NaNs on the
'normal' keyword.

* test(frontend): honest fixture text and explicit visibility timeout in subtask spec

Review nits: the long-title fixture lifted the stopped test's human turn
whose text narrates the stop scenario; give it its own LONG_TASK_USER_TEXT
and override content alongside id and tool_calls. Add the missing 15s
timeout on the running-375px title visibility assertion so a future
reorder doesn't turn the 5s default into a cold-start flake.
…ytedance#4867)

* fix(skills): reject a blank SKILL.md description at the write gate

_validate_skill_frontmatter only applied its description rules when the
value was truthy, so a blank or whitespace-only description passed the
gate. The loader rejects it, so the file was written to disk and then
disappeared from every consumer.

On the PUT edit endpoint that meant the write committed first and the
response was a 404 -- the previously working skill was gone with no
rollback. Same shape via the .skill install path and the agent-facing
skill_manage tool, which reported success for a skill that never loads.

Empty names were already rejected; description was the only field where
the write gate and the loader disagreed.

* test(skills): pin rollback rejection of a blank-description history entry

Add a regression test for the rollback path: restoring a history entry
whose stored content has an empty description must return 400
("Description cannot be empty") and leave the on-disk SKILL.md untouched,
rather than the previous destructive path that wrote the unloadable
content and then 404'd. On main this returns 404, so the test also pins
the intended status-code change on this path.
…ytedance#5138)

* chore(doc):update the CHANGLOG with latest changes

* chore(doc):update the CHANGLOG_zh.md with the change of CHANGLOG.md
… and glob in remote providers (bytedance#4980)

* fix(sandbox): stop stripping filenames when parsing find output in remote providers

The list_dir and glob parsers in the e2b, OpenSandbox, AIO, Tenki, and
BoxLite providers called .strip() on every line of find output. A
filename that legitimately ends (or begins) in whitespace was corrupted,
so the listed path never resolved on any follow-up file API call, and
the remote providers diverged from LocalSandbox, which preserves such
names via pathlib.

splitlines() already removes the line terminators, so filter empty lines
only and keep each entry verbatim. Same class of bug as the e2b
_sync_outputs_to_host fix (bytedance#4861), applied to the search parsers.

Adds a trailing-space regression test per provider at the seam each
suite already uses.

* fix(sandbox): split find output on \n only, and rename the tenki test

Review follow-ups from willem-bd:

- aio_sandbox.list_dir used str.splitlines(), which also breaks records on
  \v, \f, \x1c-\x1e and \x85 - all legal inside a Linux filename, and all
  contrary to this PR's own rule that the newline is the only delimiter.
  find emits \n and nothing else, so split("\n") is the correct parse.
- Renamed test_search_preserves_trailing_space_in_filename to
  test_list_dir_and_glob_preserve_trailing_space_in_filename, matching the
  sibling tests in test_opensandbox_provider.py and test_boxlite_provider.py.
  The body covers list_dir and glob; it never touches grep.
* docs: align custom agent naming with API

* docs: document custom agent API gate

* docs: correct custom agent storage guidance

* docs: correct custom agent config path

* docs: clarify custom agent name scope

* docs: align agent storage placeholder

* docs: clarify custom agent file updates

* docs: qualify custom agent storage

* docs: clarify agent database storage

* Fix formatting in docs-links.test.ts

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(observability): persist loop detection events

* fix(observability): persist subagent loop events

* fix(observability): narrow subagent loop event bridge

* fix(observability): attribute subagent loop events

* fix(tests): isolate subagent executor imports
* feat(ci): split backend unit tests into parallel CI shards

Split the single offline backend `make test` job into four GitHub Actions
matrix shards (SPLITS=4, GROUP=1..4) via pytest-split, so the ~12k-test suite
runs in parallel instead of in one 15-minute job. Each shard runs on its own
runner with its own Postgres/Redis services; fail-fast: false lets a failing
shard report its owned tests without cancelling its peers.

`make test` stays the canonical full-suite entry point; CI now calls the new
`make test-shard SPLITS=4 GROUP=N`. tests/blocking_io remains owned solely by
the dedicated blocking-I/O workflow (excluded via --ignore), extending bytedance#5105.

Fixes bytedance#5088

* test(ci): make backend test shards duration-aware and pin the contract

Make `make test-shard` an explicit least_duration split that READS
backend/.test_durations (read-only for shards, so concurrent CI jobs never
race writes on it), and add `make test-shard-durations` to regenerate that
file from the full offline suite. Update the CI unit-test workflow contract to
call `make test-shard SPLITS=4 GROUP=<n>` and assert the shard command carries
--splits 4, --group 2, -m "not live", --ignore=tests/blocking_io and
--splitting-algorithm least_duration. Verified on the real 13,140-test normal
suite that the four shards are pairwise disjoint and their union equals the
unsplit suite.

Refs bytedance#5088

* test(ci): fail fast when the duration baseline is missing

`make test-shard` now requires backend/.test_durations and exits with a clear
error instead of letting pytest-split silently degrade to an even (count-based)
split. Harden the CI contract test to pin `--durations-path=.test_durations` and
to assert the repo ships the committed duration baseline.

Refs bytedance#5088

* docs: trim backend/AGENTS.md within guidance budget

* test(ci): add backend test duration baseline

Add the duration baseline generated by a full offline backend run on a
GitHub-hosted ubuntu-latest runner (the same runner type the shards use), so
`make test-shard` balances the four matrix shards by real wall-clock cost.

Refs bytedance#5088

* test(ci): make the duration writer honor DURATIONS_FILE

`test-shard-durations` now writes `--durations-path=$(DURATIONS_FILE)` instead of a
hard-coded .test_durations, so the reader and writer stay consistent when the
path is overridden.

Refs bytedance#5088

* test(ci): address sharding review feedback

* test: isolate subagent execution capacity state

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
…#4952)

* fix(agents): do not hide invalid config with file fallback

* test(agents): cover invalid on-disk config fallback

* fix(agents): resolve stores off the event loop

* fix(agents): distinguish missing nested config from main config

* fix(agents): reject missing explicit config path

* test(agents): isolate config fallback test

* test(agents): isolate router blocking IO coverage

* test(agents): pin malformed config.yaml parse-error propagation

An unparseable config.yaml used to be swallowed by the broad
except Exception and silently downgrade to FileAgentStore. The
narrowed except FileNotFoundError already propagates
yaml.ParserError/ScannerError; pin that contract with a real
on-disk config instead of monkeypatched get_app_config.
)

* fix(title): ignore upload context in conversation titles

* fix(title): cover attachment-only conversations

* fix(title): skip model for attachment-only messages

* fix(title): handle whitespace-only user content

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Amazingjun-j and others added 28 commits September 4, 2026 20:20
* fix(runtime): reclaim idle keyed locks safely

Replace the per-loop thread lock registries with a waiter-aware keyed lock table. Count holders and queued waiters before acquisition so idle entries can be reclaimed without allowing a late caller to bypass an existing waiter.

Add regression coverage for runtime call-site reclamation, goal/checkpoint domain independence, queued-waiter ordering, cancellation cleanup, high-cardinality key reclamation, and cross-event-loop isolation.

Fixes bytedance#5171

* style(runtime): format keyed lock helper
…se (bytedance#5166)

* feat(gateway): add /health/ready readiness probe backed by the database

## Why

GET /health only proves the process is up: it returns 200 even when the persistence engine cannot reach the database. Orchestrators already treat it as a readiness gate (docker-compose.yaml marks the gateway service healthy and nginx depends_on service_healthy), so a DB outage or a still-migrating Postgres leaves the stack 'healthy' while every request fails.

## What changed

- New GET /health/ready endpoint: bounded SELECT 1 against the existing persistence engine (deerflow.persistence.engine.get_engine) with a 2s timeout.

- Response is 200 {'status': 'ready', 'database': 'ok'} when reachable, 503 {'status': 'degraded', 'database': 'unreachable'} when the probe fails, and 200 ready with database=not_configured for backend=memory (nothing to probe).

- GET /health is unchanged (pure liveness), and /health/ready is public through the existing /health auth whitelist.

- docker-compose.yaml gateway healthcheck now polls /health/ready so service_healthy reflects database reachability.

- Documented both endpoints in backend/app/gateway/AGENTS.md.

## Surface area

- [x] Backend API - new GET /health/ready endpoint under backend/app/gateway

- [x] Sandbox / Docker - gateway healthcheck in docker/docker-compose.yaml now gates on readiness

- [ ] Frontend UI / Agents / Skills / Dependencies

- [x] Default behavior change - existing /health unchanged; the prod compose healthcheck is stricter (503 while the database is unreachable)

## Validation

- New unit tests in backend/tests/test_gateway_health.py cover ok / unreachable / not_configured probe results and the 200/503 payload mapping (6 passed).

- app.gateway.app imports cleanly and registers both /health and /health/ready.

- ruff check + ruff format clean.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** design, implementation, and unit tests produced with AI assistance; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(helm): point the gateway readiness probe at /health/ready

## Why

Review on bytedance#5166 (willem-bd, P1): the chart still probed /health for readiness,
so Kubernetes marked the pod ready and routed traffic while the database was
unreachable - exactly the failure mode /health/ready was added to catch.

## What changed

- deploy/helm/deer-flow/templates/gateway-deployment.yaml: readinessProbe
  httpGet.path now hits /health/ready (DB-backed, 503 while the database is
  unreachable). The liveness probe stays on /health.

## Verification

- One-line path change inside the existing readinessProbe block; git diff
  confirms only the readiness path changed (liveness untouched).

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** implemented the reviewer-requested probe path change; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(gateway): readiness probe also checks the effective checkpointer/Store backend

## Why

Follow-up review on bytedance#5166 (willem-bd, P1): get_engine() only represents the
ORM backend selected by `database:`. The legacy `checkpointer:` section takes
precedence for the LangGraph checkpointer and Store, so a split configuration
(a local SQLite/memory `database:` with `checkpointer.type: postgres`) could
report 200 while the PostgreSQL backend agent runs depend on was down.

## What changed

- GET /health/ready now probes both persistence halves: the ORM engine behind
  `database:` (unchanged) and the effective LangGraph checkpointer/Store
  backend resolved with the runtime's own rule (legacy `checkpointer:` config,
  otherwise derived from `database:`), for memory/sqlite/postgres backends.

- The payload gains a `checkpointer` field with the same
  ok / not_configured / unreachable vocabulary as `database`; 503 degraded is
  returned when either probe is unreachable.

- Probes are bounded by the existing 2s timeout: sqlite via aiosqlite SELECT 1
  on the resolved path, postgres via a bounded psycopg AsyncConnection SELECT 1
  on the DSN with the configured search_path. A missing driver for a configured
  backend degrades readiness (the runtime could not run either).

- Documented the two-probe semantics in the endpoint docstring and
  backend/app/gateway/AGENTS.md.

## Verification

- New tests: healthy ORM engine + unreachable legacy checkpointer backend ->
  503 degraded with database: ok / checkpointer: unreachable; checkpointer
  probe mapping for memory/sqlite(postgres missing-driver) backends; existing
  payload tests now pin the checkpointer field.

- cd backend && python -m pytest tests/test_gateway_health.py: 11 passed.

- app.gateway.app imports cleanly; ruff check + ruff format clean.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** design, implementation, and unit tests produced with AI assistance; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(gateway): bound /health/ready to one deadline and probe the startup checkpointer snapshot

## Why

Second round of review on bytedance#5166 (zhfeng P1/P2, willem-bd P1/P1/P2). Three
correctness issues remained in the readiness endpoint:

- The database and checkpointer probes ran sequentially, each allowed 2s, so a
  healthy response could take almost 4s - past Kubernetes' 1s default
  readinessProbe timeout and inside Docker's 3s client timeout. A slow but
  healthy backend could make every replica unready.

- The checkpointer probe re-resolved process-wide, hot-reloaded configuration
  per request, while app.state.checkpointer/store are built once from the
  startup_config snapshot in langgraph_runtime(). After a live config edit the
  endpoint could probe a backend the running gateway does not use, and a
  resolution failure was swallowed into None -> not_configured -> 200.

- The SQLite probe opened the path with aiosqlite.connect(), which creates the
  file when missing: a deleted checkpoint database was silently resurrected as
  an empty file and reported ok instead of surfacing the outage.

## What changed

- backend/app/gateway/health.py: the two probes now run concurrently beneath a
  single endpoint-wide deadline (_READINESS_DEADLINE_SECONDS=3.0) so a healthy
  response completes within one probe window (~2s), never the sum of both.
  A probe that overruns the deadline degrades the endpoint instead of hanging.

- langgraph_runtime() now records the checkpointer/Store config resolved from
  the same startup_config snapshot its checkpointer/store singletons are built
  from (app.state.checkpointer_config); /health/ready probes that snapshot and
  never re-resolves hot-reloaded config. resolve_checkpointer_config() returns
  None on resolution failure and the endpoint fails closed (503, checkpointer:
  unreachable) instead of reporting not_configured.

- The SQLite probe opens disk-backed paths with the non-creating mode=rw URI
  flag, so a missing database file stays missing and yields unreachable;
  in-memory forms (:memory:, file:...mode=memory) have nothing external to
  probe and report not_configured like the memory backend.

- Orchestrator timeouts now sit above the endpoint bound: Helm readinessProbe
  gains timeoutSeconds: 5 (Kubernetes default is 1s) and the docker-compose
  gateway healthcheck client timeout moves from 3s to 5s.

## Verification

- New regression tests: concurrent probes keep total elapsed time within one
  probe window; a probe ignoring its budget trips the endpoint deadline to 503;
  missing SQLite file stays absent and yields unreachable; in-memory SQLite
  forms map to not_configured; missing startup snapshot / config resolution
  failure fail closed to 503; resolve_checkpointer_config() raising is covered.

- cd backend && python -m pytest tests/test_gateway_health.py: 21 passed;
  tests/test_gateway_docs_toggle.py and lifespan/shutdown gateway suites pass.

- ruff check + ruff format clean on all changed files.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** implemented the reviewer-requested concurrency/deadline, startup-snapshot probing, fail-closed resolution, and non-creating SQLite probe; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(gateway): serialize connection-opening readiness probes behind a strict gate

## Why

Review on bytedance#5166 (willem-bd, P1): every request to /health/ready opened a new
PostgreSQL connection in _probe_postgres_backend, outside both the ORM pool and
the runtime checkpointer pool. The route is public through the /health auth
prefix and nginx proxies /health/*, so concurrent unauthenticated requests
could create an unbounded number of connections (each held for up to two
seconds), exhaust PostgreSQL max_connections, and take down both normal
traffic and the readiness probe itself.

## What changed

- backend/app/gateway/health.py: connection-opening checkpointer probes
  (sqlite connect, postgres AsyncConnection.connect) now run inside a strict
  per-process gate - an asyncio.Lock cached per running event loop - so at
  most one probe connection can be in flight per worker process. Requests
  that queue behind the gate are still shed by the existing endpoint-wide
  deadline, so a flood cannot pile up new connections or open files.

- Memory and unknown-backend decisions stay outside the gate; payload and
  probe semantics are unchanged. The serialization is documented in the
  module docstring and backend/app/gateway/AGENTS.md.

## Verification

- New regression test: 8 concurrent readiness_payload() requests against an
  instrumented sqlite probe assert the maximum number of in-flight probe
  connections is 1 while every request still returns 200.

- cd backend && python -m pytest tests/test_gateway_health.py: 22 passed;
  tests/test_gateway_docs_toggle.py and tests/test_gateway_lifespan_shutdown.py
  also pass on the merged main head.

- ruff check + ruff format clean on all changed files.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** implemented the reviewer-requested strict concurrency bound for the public readiness probe; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.
…tarts the frontend on Windows (bytedance#5181)

* bugfix bytedance#5179

* test: cover the env-aware _pick_python fallback from bytedance#5179

Follow the test_serve_nginx_stop.py extraction pattern: drive the real
_pick_python from serve.sh against a stub-only PATH plus a mocked env.

- python3 succeeds directly but fails through env -> python selected
  (red on main, green on this branch)
- env rejects every candidate -> nonzero exit (also red on main)
- healthy PATH with the real env -> python3 preferred, guarding against
  over-rejection

MSYS/Git Bash hosts need the stub dir as an MSYS-style (/c/...) PATH
entry, and bash diagnostics may arrive in the console code page, so the
runner decodes output with errors="replace".
…tedance#5154)

* fix(agents): make the injected current-date timezone configurable

## Why

The date reminder injected into the lead and subagent prompts (DynamicContextMiddleware / SubagentDateContextMiddleware) was formatted with the server's local wall clock. DeerFlow containers default to UTC, so a user in Asia/Shanghai chatting in the 00:00-08:00 window was told that 'today' is the previous day - the model then reasons, plans, and date-stamps against the wrong day.

## What changed

- _format_current_date() now reads the optional DEER_FLOW_DATE_TIMEZONE env var (IANA name, e.g. Asia/Shanghai) and renders the date in that zone.

- Unset = unchanged server-local behavior; invalid names log a warning and fall back to server-local.

- Documented the knob in config.example.yaml, the module docstring, and the DynamicContext entry in agents/middlewares/AGENTS.md.

## Surface area

- [x] Agents / LangGraph - prompt-layer date context only; message shape and midnight-update behavior unchanged

- [ ] Frontend UI / Backend API / Sandbox / Skills / Dependencies

- [x] Default behavior change (opt-in via env var - no behavior change unless set)

## Bug fix verification

- New tests: test_format_current_date_honors_configured_timezone (UTC 20:30 -> 2026-09-03 in Asia/Shanghai), test_format_current_date_defaults_to_server_local_without_env, test_format_current_date_invalid_timezone_falls_back.

- Existing mocked-datetime tests pass unchanged (no env -> datetime.now() path).

## Validation

- cd backend && python -m pytest tests/test_dynamic_context_middleware.py: 31 passed.

- blocking_io/test_dynamic_context_middleware.py: 2 pre-existing abefore_agent failures reproduce identically on clean main (blockbuster os.listdir detection on this host); the other 2 pass.

- ruff format + ruff check clean.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** analysis, implementation, and regression tests produced with AI assistance; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(agents): avoid passing tz to datetime.now when no timezone is configured

CI (backend-unit-tests shard 2) failed in test_tool_error_handling_middleware.py::test_subagent_chain_injects_date_without_memory_and_coalesces_for_strict_provider because its _FrozenDateTime.now() subclass override accepts no arguments, while _format_current_date() called datetime.now(None) even when DEER_FLOW_DATE_TIMEZONE was unset.

- _format_current_date() now calls datetime.now() with no arguments unless a timezone is actually configured, preserving the exact legacy call shape for every datetime-subclass test fake.

- The configured-zone path still calls datetime.now(tz) and converts via astimezone(tz).

- Updated the no-env unit test to assert datetime.now() is called without arguments.

Validation: python -m pytest tests/test_dynamic_context_middleware.py + the previously failing strict-provider test: 32 passed. ruff clean.

* fix(agents): declare the effective current-date timezone in the assembly descriptor

## Why

Maintainer review on the DEER_FLOW_DATE_TIMEZONE change (bytedance#5154): the knob is
prompt-affecting, yet both DynamicContextMiddleware and SubagentDateContextMiddleware
were invisible to the agent assembly descriptor - describe_middleware() fell back to
{"probed": true} for unset, UTC, and Asia/Shanghai alike, so deployments that inject
different dates shared one assembly fingerprint and release observers could not
distinguish or audit the behavior change.

## What changed

- Both middlewares now implement release_policy_parameters() -> dict[str, object],
  declaring {"current_date_timezone": <name>} as required by the module's middleware
  self-description contract.

- The declared value is the normalized effective zone: a configured, valid
  DEER_FLOW_DATE_TIMEZONE is reported by its IANA key (ZoneInfo.key); otherwise the
  server-local zone is resolved to its IANA key when the platform exposes one and to
  its tzname label otherwise (fixed-offset hosts), with "UTC" as the final fallback.

- Added both middlewares to _MIDDLEWARE_DECLARATIONS in
  backend/tests/test_middleware_release_policy.py so the existence check and the
  construct-and-canonical-hash check cover them.

## Verification

- New tests: test_date_middlewares_declare_configured_timezone (Asia/Shanghai),
  test_date_middlewares_declare_utc_timezone, plus resolved-server-local assertions
  for the unset and invalid-env paths; both middlewares agree in every case.

- cd backend && python -m pytest tests/test_dynamic_context_middleware.py
  tests/test_middleware_release_policy.py: 70 passed.

- Regression spot-check: tests/test_agent_assembly_descriptor.py,
  tests/test_tool_error_handling_middleware.py, tests/test_system_message_coalescing_middleware.py:
  102 passed.

- ruff check + ruff format clean.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** analysis, implementation, and regression tests produced with AI assistance; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(agents): stabilize the declared date timezone and simplify the formatting path

## Why

Follow-up review on bytedance#5154 (willem-bd). The release-policy declaration added in
884cec4 resolved the observability gap but pinned far less identity than its
docstrings claimed, and the formatting path carried a production no-op.

## What changed

- The declared label is now stable and unambiguous: a configured, valid
  DEER_FLOW_DATE_TIMEZONE is reported by its IANA key; without one, the
  server-local zone is resolved to a real IANA key from the TZ env var or the
  /etc/localtime symlink (Linux/macOS); when no key is recoverable (Windows,
  stripped containers) the declaration falls back to a stable
  `server-local(+-HH:MM)` sentinel carrying the current UTC offset. It never
  reports a bare abbreviation - datetime.now().astimezone() yields only a
  fixed-offset timezone whose tzname (e.g. CST, EST/EDT, CET/CEST) is
  ambiguous or DST-churns, which the assembly descriptor docstring says must
  not happen.

- Dropped the redundant astimezone(tz) in _format_current_date():
  datetime.now(tz) already returns the instant expressed in tz. The
  configured-zone test now fakes datetime.now(tz) semantics (the fixed instant
  converted into the requested zone) instead of relying on that conversion.

- Documented why the knob is an env var, not a config-schema field: it is read
  at runtime by both date-context middlewares so an operator can point a
  container at another zone without mounting a config.yaml (module docstring +
  config.example.yaml note).

- AGENTS.md: fixed the glued DynamicContext sentence (missing separator).

- Added tzdata>=2025.1 to the harness runtime dependencies (with uv.lock) so
  ZoneInfo works on stripped containers / Windows without an OS zone database.

## Verification

- New tests: test_server_local_timezone_name_reads_tz_env,
  test_effective_timezone_sentinel_uses_offset_when_local_zone_is_not_resolvable;
  reworked test_format_current_date_honors_configured_timezone to exercise the
  real datetime.now(tz) path.

- cd backend && python -m pytest tests/test_dynamic_context_middleware.py
  tests/test_middleware_release_policy.py tests/test_agent_assembly_descriptor.py
  tests/test_tool_error_handling_middleware.py: 140 passed.

- ruff check + ruff format clean.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** analysis, implementation, and regression tests produced with AI assistance; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(agents): offload subagent date injection off the event loop

## Why

Follow-up review on bytedance#5154 (willem-bd, P2): SubagentDateContextMiddleware.abefore_agent()
called _inject() directly, so enabling DEER_FLOW_DATE_TIMEZONE could synchronously
read the OS timezone database (or the tzdata wheel) on a cold cache - filesystem
work on the async subagent execution path whenever no assembly observer resolved the
zone first.

## What changed

- SubagentDateContextMiddleware.abefore_agent() now offloads the injection via
  asyncio.to_thread with the same bounded timeout DynamicContextMiddleware uses
  (issue bytedance#3402); on timeout it logs and skips the date update for that run instead
  of blocking the loop.

- Narrowed the exception handling in _date_timezone() and the TZ-env branch of
  _server_local_timezone_name() to configuration-shaped failures
  (ZoneInfoNotFoundError / ValueError / OSError). Previously a blanket
  `except Exception` also swallowed BlockingError raised by the blocking-I/O
  regression gate, mislabeling a loop-blocking call as an invalid timezone and
  silently degrading to server-local - which made the new regression anchor
  useless. Other exceptions now propagate.

## Verification

- New blocking-I/O regression anchor
  (backend/tests/blocking_io/test_subagent_date_context_middleware.py): drives a
  real create_agent graph under the strict Blockbuster gate with the knob enabled
  and asserts the date reminder is injected. Verified it fails (BlockingError) when
  the offload is reverted and passes with it in place.

- python -m pytest tests/blocking_io/test_subagent_date_context_middleware.py:
  1 passed. The two pre-existing os.listdir failures in
  tests/blocking_io/test_dynamic_context_middleware.py reproduce unchanged on this
  host (same as clean main).

- python -m pytest tests/test_dynamic_context_middleware.py
  tests/test_middleware_release_policy.py tests/test_tool_error_handling_middleware.py
  tests/test_agent_assembly_descriptor.py: 139 passed; the single
  ToolReceiptMiddleware-ordering failure reproduces with the change stashed
  (local extensions registry, unrelated to this PR).

- ruff check + ruff format clean.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** analysis, implementation, and regression tests produced with AI assistance; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

* fix(agents): read the direct /etc/localtime symlink target for the zone key

## Why

Follow-up review on bytedance#5154 (willem-bd, P2): on macOS, /etc/localtime commonly
points to /var/db/timezone/zoneinfo/<zone>, but Path.resolve() follows that
directory's own symlink and yields a versioned path such as
/private/var/db/timezone/tz/2026c.1.0/zoneinfo/Asia/Shanghai, which matched no
configured prefix. The server-local resolution then returned None and the
assembly descriptor fell back to a server-local(+HH:MM) sentinel even though
the IANA key was available - conflating zones that share an offset and making
DST-based fingerprints unstable.

## What changed

- _server_local_timezone_name() now reads the direct symlink target via
  os.readlink("/etc/localtime") instead of Path.resolve(), so macOS' unversioned
  zoneinfo path is seen as-is and its IANA key is preserved.
- The zone key is taken from whatever follows the last "/zoneinfo/" segment,
  which also handles Apple's canonical versioned path when a direct target
  already carries it, and relative targets are normalized against /etc.
- Removed the now-unused Path import and the fixed zoneinfo prefix tuple.

## Verification

- New tests: test_server_local_timezone_name_reads_direct_macos_symlink_target,
  test_server_local_timezone_name_reads_apple_versioned_symlink_target, and
  test_server_local_timezone_name_normalizes_relative_symlink_target.

- python -m pytest tests/test_dynamic_context_middleware.py
  tests/test_middleware_release_policy.py tests/test_agent_assembly_descriptor.py:
  105 passed (75 after re-running the first two on the merged main). The blocking
  subagent anchor still passes; the two pre-existing os.listdir blocking failures
  on this host are unchanged.

- ruff check + ruff format clean.

## AI assistance

**Tool(s) used:** Codex (coding agent)

**How you used it:** analysis, implementation, and regression tests produced with AI assistance; reviewed before commit.

- [ ] I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(sandbox): add controlled egress approvals

* Apply batched suggestions from code review

* fix(sandbox): harden restricted network policy

* fix(sandbox): harden denied egress handling

* fix(sandbox): isolate network proxy sidecar

* chore: retry sandbox image smoke

* fix(sandbox): close remaining network policy gaps

* fix(sandbox): harden relay token rejection

* fix(sandbox): fence incompatible policy replacement

* fix(sandbox): replace containers across network modes

* fix(sandbox): close remaining lifecycle gaps

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(observability): persist deferred tool promotions

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(ci): trim agent guidance chain

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
…eaming (bytedance#5195)

In tool-enabled requests MindIEChatModel._astream falls back to awaiting the full _agenerate response and re-emitting it as simulated AIMessageChunks. The full response carries usage_metadata, but none of the simulated chunks copied it, so chunk aggregation (add_ai_message_chunks) produced a final message with usage_metadata=None. Token usage therefore vanished from token accounting, run stats, persistence and the UI for every tool-enabled streamed turn.

Mirror OpenAI's terminal-usage-frame convention: attach msg.usage_metadata to exactly the last simulated chunk (the trailing tool-call chunk when present, else the last text chunk / the single tool-only chunk) so the aggregated message carries it exactly once. add_usage() is per-chunk additive, so attaching usage to every chunk would multiply the totals.

Scope: MindIEChatModel only; other providers keep native streaming and ainvoke/non-tool astream were already correct.

Tests: regression guard asserting exactly one carrier chunk equals the last one and that merged usage equals the original across all three simulated-stream branches, plus chain-level tests driving the public astream() wrapper and asserting the persisted model_dump() shape.

Closes bytedance#5192
…dance#5191)

* fix(runtime): finish terminal signaling after hook cancellation

* fix(runtime): shield task-stop observer fan-out

---------

Co-authored-by: CorgiBoyG <CorgiBoyG@users.noreply.github.com>
…#5141)

* fix(lark): enforce private ACLs on Windows credential tree

On Windows, posix chmod(0o700/0o600) does not map to NTFS ACLs, so the
secret-bearing Lark CLI credential tree was not actually owner-restricted
and existing trees were not repaired.

Branch the permission application by platform:
- POSIX: directories 0o700, files 0o600 (behavior unchanged).
- Windows: disable inherited ACLs, grant the Gateway process user Full
  Control (resolved via its SID from whoami /user /fo csv /nh so it is
  locale-independent), and remove broad non-administrative grants
  (Everyone, Authenticated Users, Users). Fail closed on identity or
  icacls failures so a tree is never left accessible silently.

Existing-tree handling is covered by asserting every entry in the tree is
repaired, and the Windows command contract is covered by mocked tests run
in CI.

* fix(lark): harden Windows credential tree against TOCTOU and hard-link races

This replaces the path-based Windows hardening (lstat -> SetFileSecurityW(path) -> iterdir) with a handle-relative walker, so validation, the ACL update, and traversal are bound to the opened object rather than a re-resolved pathname.

Every credential object is opened no-follow; children are enumerated with GetFileInformationByHandleEx(FileFullDirectoryInfo) and opened/created relative to an already-open parent handle (NtOpenFile/NtCreateFile with OBJECT_ATTRIBUTES.RootDirectory), so a pathname swap cannot redirect the walk. Credential directories are opened exclusively (share=0): SetSecurityInfo therefore does not propagate the final owner-only OI|CI DACL into as-yet-unvalidated children, and the namespace is locked for the duration of the walk (concurrent child rename/replacement and hard-link insertion fail with sharing violations). Any file with nNumberOfLinks != 1 is rejected before its security descriptor is touched, so an NTFS hard link to an external file cannot change that file owner/DACL. POSIX keeps the lstat-before-descent walk.

Tests: native regressions for exclusive no-propagation, late hard-link insertion being blocked, mid-walk junction swap being blocked, static hard-link rejection, and both real NTFS junction rejections. Mock seams updated for the handle-relative API, and Windows portability fixes make the suite green on Windows except the known bytedance#5116 sandbox-runtime executable-bit failures.

* test(lark): keep the credential-tree symlink assertion portable

The credential-tree symlink rejection is a ValueError; POSIX reports a symlink while the Windows handle-relative walker reports a reparse point. Use a platform-dependent regex so the test passes on Linux/macOS and Windows.

* fix(lark): close remaining credential-tree hardening gaps

Review follow-up for the handle-relative credential-tree walker:
- Stage the transaction snapshot under the owner-only root, copying only config/ and data/.
- Serialize ensure() per-user across threads and processes with a dedicated lock.
- Make the walker iterative so deep trees cannot hit the recursion limit.
- Re-reject a symlinked POSIX root before mkdir; drop the over-strict ancestor-chain check.
- Soften the SetSecurityInfo failure claim; add regressions for each and carry os.SEEK_END in the os stub.

* fix(lark): anchor hardening lock under trusted base and keep POSIX untouched

Follow-up refinements to the credential-tree hardening:
- The per-user hardening lock file now lives directly under the trusted base_dir
  instead of the unverified per-user chain, so it is never written through an
  ancestor that has not yet passed reparse validation.
- ensure() takes the hardening lock only on the Windows branch; POSIX keeps the
  original contract, so no new lock-file side effect.
- Strengthen the ancestor-junction regression (lock not written to the external
  target) and fix two test docstrings to match the parent-first order and the
  no-prior-broadening failure claim.

* fix(lark): anchor credential-operation lock under trusted base on Windows

The per-user credential lock (_lark_credential_lock) created its advisory lock file
under the unverified per-user chain (users/<id>/integrations/.lark-cli.credentials.lock)
before ensure() validated the ancestor chain. On Windows it is now anchored directly
under the trusted paths.base_dir (mirroring the hardening lock), so a junction at
integrations can no longer cause the credential lock to be written into an external
target before reparse validation. POSIX keeps the original location unchanged.

Tests:
- Public-flow regression (start_lark_config -> credential lock -> ensure) uses an empty
  sentinel lock file to prove the old credential-lock path is never opened/written.
- CLI-write re-harden tests restore the POSIX outcome assertion (file tightened to 0600).
* fix(memory): enforce backend read failure policy

* fix(memory): harden failure policy handling

* fix(memory): narrow strict read handling

* fix(memory): keep timeout handling off saturated executor

* fix(memory): preserve legacy fail-closed timeouts
…arity (bytedance#5210)

test_list_by_thread creates two runs back-to-back and expects the newer
one to sort first under list_by_thread's newest-first ordering. That
relies on the wall clock advancing between the two create() calls.

On Windows, datetime.now() has a coarse granularity (~15.6 ms), so both
runs can receive an identical created_at. Python's stable sort then
keeps insertion order and the assertion fails; on this host 50
consecutive now_iso() calls return identical strings.

Drive the clock with a controlled 1 ms-per-call fake (same monkeypatch
pattern as test_list_by_thread_is_stable_when_timestamps_tie) so the
strictly-newer assumption no longer depends on the host clock. The tie
case stays covered by the existing dedicated test.

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
bytedance#5187)

* fix(journal): dedup llm.ai.response persistence on re-fired on_llm_end

LangChain may deliver on_llm_end more than once for the same run_id.
RunJournal already dedups token accounting and the run summary
(_record_message_summary) on that premise via _counted_message_llm_run_ids,
but the durable llm.ai.response self._put() call was left unguarded.

The event store is append-only and count_messages/list_messages read raw
rows without read-time dedup, so a replayed callback persists a second
llm.ai.response row for one logical response while the run's own
message_count counts it once. This inflates count_messages, duplicates a
message in list_messages pagination, and leaves the durable feed
inconsistent with the run summary.

Gate the persistence + summary block by the existing per-run_id guard so a
replayed callback is a no-op, keeping the durable message feed and the run
summary in agreement. Distinct run_ids are unaffected.

Adds regression tests: a re-fired callback for one run_id persists exactly
one row (red on main), and distinct run_ids each still persist a message.

* fix(journal): preserve canonical response on late usage

* fix(journal): preserve late usage while deduplicating responses

* fix(journal): keep first callback response canonical

* fix(journal): snapshot canonical response summaries

---------

Co-authored-by: CorgiBoyG <CorgiBoyG@users.noreply.github.com>
…bytedance#5211)

* fix(tests): make three backend test modules runnable on Windows hosts

Follow-up to bytedance#5210 (clock-granularity fix) clearing the remaining
deterministic Windows failures in modules that are otherwise
platform-neutral. Five tests fail on Windows for root causes unrelated
to the behavior under test:

- test_pnpm_script.py::test_make_install_dry_run_does_not_invoke_bare_pnpm
  shells out to `make`, which Git Bash on Windows does not bundle
  (the same gap reported in bytedance#5177). Skip when make is unavailable.
- test_mcp_session_pool.py: one test asserts the injected MCP temp dir
  has POSIX mode 0o700; Windows has no POSIX mode bits (ntfs reports
  0o777), so the mode check now runs only on POSIX. A second test
  asserted `TMP.endswith("mcp-internal/tmp")` while Windows tmp paths
  use backslashes; normalize the separator before comparing.
- test_skillscan_native.py: two tests build a 3000-operand `1+1+...`
  chain to exercise deep-AST resilience. CPython's C recursion limit
  for ast construction is platform-dependent (~800 on Windows vs
  ~8000 elsewhere), so 3000 reliably overflows on Windows and the
  scanner records an error instead of findings. 600 chained BinOps
  stays deep for the client-analysis walk while fitting the limit on
  every supported platform.

No product code is touched; on POSIX the suite behaves exactly as
before.

* fix(tests): make three backend test modules runnable on Windows hosts

Follow-up to bytedance#5210 (clock-granularity fix) clearing the remaining
deterministic Windows failures in modules that are otherwise
platform-neutral. Five tests fail on Windows for root causes unrelated
to the behavior under test:

- test_pnpm_script.py::test_make_install_dry_run_does_not_invoke_bare_pnpm
  shells out to `make`, which Git Bash on Windows does not bundle
  (the same gap reported in bytedance#5177). Skip when make is unavailable.
- test_mcp_session_pool.py: one test asserts the injected MCP temp dir
  has POSIX mode 0o700; Windows has no POSIX mode bits (ntfs reports
  0o777), so the mode check now runs only on POSIX. A second test
  asserted `TMP.endswith("mcp-internal/tmp")` while Windows tmp paths
  use backslashes; normalize the separator before comparing.
- test_skillscan_native.py: two tests build a 3000-operand `1+1+...`
  chain to exercise deep-AST resilience. CPython's C recursion limit
  for ast construction is platform-dependent (~800 on Windows vs
  ~8000 elsewhere), so 3000 reliably overflows on Windows and the
  scanner records an error instead of findings. 600 chained BinOps
  stays deep for the client-analysis walk while fitting the limit on
  every supported platform.

No product code is touched; on POSIX the suite behaves exactly as
before.

Update: address review feedback (P2, recursion-recovery regression)

The 600-operand chain no longer exercises recursion exhaustion on POSIX,
so the recovery handler in _scan_python was unprotected by the renamed
test. Replace the input-based variant with a controlled RecursionError
injected via monkeypatched _find_client_handle_sink (platform-
independent); removing the handler now turns the test red again.

* fix(tests): make three backend test modules runnable on Windows hosts

Follow-up to bytedance#5210 (clock-granularity fix) clearing the remaining
deterministic Windows failures in modules that are otherwise
platform-neutral. Five tests fail on Windows for root causes unrelated
to the behavior under test:

- test_pnpm_script.py::test_make_install_dry_run_does_not_invoke_bare_pnpm
  shells out to `make`, which Git Bash on Windows does not bundle
  (the same gap reported in bytedance#5177). Skip when make is unavailable.
- test_mcp_session_pool.py: one test asserts the injected MCP temp dir
  has POSIX mode 0o700; Windows has no POSIX mode bits (ntfs reports
  0o777), so the mode check now runs only on POSIX. A second test
  asserted `TMP.endswith("mcp-internal/tmp")` while Windows tmp paths
  use backslashes; normalize the separator before comparing.
- test_skillscan_native.py: two tests build a 3000-operand `1+1+...`
  chain to exercise deep-AST resilience. CPython's C recursion limit
  for ast construction is platform-dependent (~800 on Windows vs
  ~8000 elsewhere), so 3000 reliably overflows on Windows and the
  scanner records an error instead of findings. 600 chained BinOps
  stays deep for the client-analysis walk while fitting the limit on
  every supported platform.

No product code is touched; on POSIX the suite behaves exactly as
before.

Update: address review feedback (P2, recursion-recovery regression)

The 600-operand chain no longer exercises recursion exhaustion on POSIX,
so the recovery handler in _scan_python was unprotected by the renamed
test. Replace the input-based variant with a controlled RecursionError
injected via monkeypatched _find_client_handle_sink (platform-
independent); removing the handler now turns the test red again.

Update: address second review feedback (P2, early-stop regression coverage)

The 600-operand tail no longer proves the walk stops after finding a
sink (it completes inside POSIX recursion limits either way). Replace it
with the suggested instrumentation: a sentinel os.system call after the
sink plus an instrumented _walk_client_scope that records any visit to
the sentinel while analysis.found is already set, failing the test if
traversal continues past the sink. Platform-independent; the sentinel's
shell-exec finding comes from the deterministic ast.walk pass and is
irrelevant to the walk guard.
…package (bytedance#5215)

TestBashExecutionHarvest's shell-persistence tests report
shell_persistent=None whenever deerflow.sandbox.overwrite is not already
cached in sys.modules. _setup_executor_classes replaces the
"deerflow.sandbox" parent package with a MagicMock, so a later
`from deerflow.sandbox.overwrite import unwrap_sandbox` inside
_harvest_shell_persistence can no longer locate the submodule through
the mocked parent ("'deerflow.sandbox' is not a package") when the leaf
module is not already in sys.modules. The helper's
`except Exception: return None` silently converts that ImportError into
an UNKNOWN provenance stamp.

Whether the leaf module was cached depended on which tests ran earlier
in the session, making the outcome order-dependent: green in CI by
collection-order luck, red when the module runs alone or first.

Fix it the same way the fixture already handles audit_context and
tool_search: preload the real leaf modules (deerflow.sandbox.sandbox_provider
and deerflow.sandbox.overwrite) before installing the mocked parent
package, pin them in sys.modules for the duration of the test, and
restore the previous state afterwards.
…dance#5222)

The Chinese README had drifted from the English one: the entire
"LangGraph Studio (Optional)" section (including the standalone
langgraph dev workflow, server-owned assistant provenance in that mode,
the persisted-store repair note, the authenticated-identity consumption
for custom agents/skills/uploads/memory, the /mnt/user-data/outputs
native-delivery enforcement, and the dual LangGraph streaming interfaces
for custom events) was never translated and had no zh counterpart.

Add the translated section in the same position it occupies in the
English README (after the local development walkthrough, before
进阶配置), keeping the en section structure aligned. Verified the
section heading now exists on both sides and that no relative links are
affected.
…_zh (bytedance#5230)

Continues the zh/en README drift follow-up (bytedance#5222 covered the LangGraph
Studio section). The English README documents Request Trace Correlation
as a standalone section between the slash-command table and LangSmith
Tracing; the zh README only mentioned the correlation id in passing
inside its Langfuse section.

Add the translated section in the same position (after the
slash-command table, before LangSmith 链路追踪), covering: X-Trace-Id
inheritance/generation and the response header, propagation to detached
runs / subagents / background memory threads, the logging.enhance
config, the deerflow_trace_id semantics (not a run id, not a provider
trace id, not a lookup key, ignored-and-overwritten when supplied by the
caller), terminal run.delivery receipts with orphan-recovery behavior,
and the loop-detection / deferred-tool-promotion run event records.
…up (bytedance#5232)

* fix(workspace-changes): drain cancelled snapshot scans

* test(workspace-changes): cover cancellation during snapshot scan

* fix(workspace-changes): consume drained scan outcome

* test(workspace-changes): keep scan cancellation regression focused

* test(workspace-changes): pin cleanup ownership under recancel
* feat(channels): select custom agents per conversation

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(channels): reserve agent slash command across clients

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(tui): hide reserved slash commands from skills

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(channels): preserve selected agent across clients

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
* feat(chats): add archive and restore

* test(chats): observe archive search requests in pagination e2e

* docs(gateway): move thread lifecycle details out of inherited guidance

* docs(chats): add concise archive and restore RFC

* docs(chats): move archive RFC discussion to issue 5237
…5206)

Signed-off-by: Beautyl0ve <74452755+Beautyl0ve@users.noreply.github.com>
* fix(llm): release owned recovery probe on cancellation

* docs: keep middleware guidance within chain budget

---------

Co-authored-by: zaoshangduziteng <309590849+zaoshangduziteng@users.noreply.github.com>
* docs: update middleware contribution examples

* docs: clarify middleware registration paths

* docs: clarify middleware injection scope

* docs: clarify middleware state updates

* docs: clarify middleware state updates

* docs: clarify middleware pipeline placement

* docs: complete middleware order guidance

* docs: align middleware guard conditions

* docs(middleware): name runtime sanitization order

* docs: pin middleware runtime order

* docs: clarify middleware assembly paths

* docs: clarify middleware anchor scope
* feat(mcp): add optional Parallel Search server

* docs(mcp): document Parallel Search opt-in and data sharing

* docs(mcp): address Parallel Search review feedback
A bookkeeping exception after execute_async() could finalize/requeue the
durable item while the previous child was still running and no longer
supervised. Retain ownership, cancel, wait for teardown, then release.

Fixes bytedance#5240

Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com>
SubagentBatchesConfig rejects poll_interval_seconds < 0.1 and
lease_seconds < 10. Keep the bookkeeping-failure overlap test inside
those schema bounds.

Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com>
Bound the supervised wait, wait for the executor teardown signal
instead of terminal status, and stop without immediate retry when the
lease is lost or renewal fails.

Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com>
@Zsanz3 Zsanz3 closed this Sep 8, 2026
@cursor

cursor Bot commented Sep 16, 2026

Copy link
Copy Markdown

Follow-up for the remaining review notes on bytedance#5253:

  1. _await_dispatched_execution_teardown() already has real False exits (wait-budget timeout, lost lease / cancel_requested, renewal exception). Those paths hit “Refusing immediate retry” and skip finalize_item, so a stuck child cannot renew forever. test_unconfirmed_teardown_does_not_release_ordinary_immediate_retry pins the timeout case.

  2. The executor regression no longer patches _aexecute. It now drives the real _aexecute_admitted finally (sandbox lease release_async) plus run_with_timeout’s mark, pauses inside release_async, and asserts ["release", "teardown_event"]. Moving mark_execution_teardown_complete() to the top of that finally fails the test.

HEAD is 078d0da6. Outdated threads that were already answered were left as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.