Repository navigation
Conversation
* 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(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>
|
Follow-up for the remaining review notes on bytedance#5253:
HEAD is |
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 returnFalse. The wait is bounded to a few lease periods; unconfirmed teardown no longer takes the ordinary immediate-retry path.SubagentResult.execution_teardown_event(set afterrun_with_timeout()returns) or a vanished registry entry — not by CANCELLED/FAILED/completed_at.valid: False/cancel_requestedstops the renew/poll loop and returnsFalseinstead of finalizing without a held lease.item_id, and treated as the lease-expire path.Regressions cover lease-lost, unconfirmed teardown, and renewal failure. HEAD is
909a53d547f81f24640d7cf3741ceeffb6ef80e7.