* feat(frontend): pin the sent message to the top of the viewport
After a send, keep the user's turn at the top of the viewport and let the
streamed reply grow downward below the reading position, instead of the
viewport chasing the bottom (ChatGPT-style send behavior).
MessageList detects a freshly appended trailing human group while a run is
active and hands its key to VirtualMessageList. The list then grows a
flexible end spacer sized to the remaining viewport, so the last row can
actually reach the top (the browser clamps the scroll without it), escapes
the stick-to-bottom lock around the alignment, and re-escapes on a timeout
chain until the resize animation from the optimistic message settles; the
spacer shrinks as the reply grows and collapses when the run finishes.
Scrolling down or the scroll-to-bottom button re-locks the follow. History
pagination, thread switches and regenerates are excluded by construction
and covered by DOM tests; Playwright covers the static and the virtualized
list against a held run stream.
* style(frontend): fix scroll-on-send test lint
* fix(frontend): send-pin review follow-ups
Review follow-ups on top of the send pin:
- Hand scroll control back on the first genuine user input: the pin chain
arms wheel/touchstart/pointerdown listeners on the scroller, and outline
navigation plus the scroll-to-bottom button cancel it through
cancelPinFollow — previously every tick reverted user scrolling for ~2s
after a send.
- Capture the mount-time tail key during the first render so the mount
absorption is idempotent under StrictMode's double effect run.
- Export LOCAL_HUMAN_MESSAGE_ID_PREFIX from core/threads/hooks and use it
at both the mint site and the pin predicate.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(frontend): keep invalid cron schedules as custom
* test(frontend): cover inclusive cron bounds
* docs(scheduled-tasks): note cron preset bounds
The form keeps an out-of-range cron as custom; the guide now says which
numeric ranges a preset covers and that saving such an expression returns
422. CHANGELOG entries cover the fix.
* docs(scheduler): clarify invalid cron save validation
---------
Co-authored-by: Nan Gao <thunderggnn@gmail.com>
* fix(web-fetch): stop sending .env.example placeholder keys to providers
make config and make setup copy .env.example into .env, and the template
set nine search/fetch provider keys to example values such as
JINA_API_KEY=your-jina-api-key. The default keyless Jina Reader sent
that text as a Bearer key, so every web_fetch on a fresh install failed
with 401 while make doctor reported web fetch as configured.
- .env.example comments those nine lines out, like every other credential
in the template already is.
- provider_credentials.resolve_api_key reads a your-...-key/-token/-secret
value as unset, so existing .env files that still hold the placeholders
recover without editing, for every provider that resolves keys there.
* chore: leave the changelog entry to merge time
Per CONTRIBUTING.md (#8202), maintainers add changelog entries when they
merge a pull request.
* fix(web-search): route every provider key and doctor through one placeholder rule
Review follow-up for #8206:
- provider_credentials exposes is_template_placeholder(), the single
definition of an .env.example example value. key/token/secret must be
a whole hyphen-separated word, so a real value such as your-monkey or
your-passkey is no longer dropped.
- Serper, Serply, Sofya, SpecStream, Unbrowse, Webz, Brave, Tencent WSA
and You.com resolve config and environment keys through
resolve_api_key instead of private strip-and-send copies, so an old
.env that still holds the template value no longer sends it.
- make doctor asks the same predicate: a placeholder key is reported as
still holding the .env.example value instead of "set", and a literal
placeholder in config.yaml falls back to the environment key as the
runtime does.
- A census test keeps credential environment reads under community/
wrapped in resolve_api_key.
* fix(web-search): match shipped placeholders exactly and mirror Browserless precedence in doctor
Second review follow-up for #8206:
- is_template_placeholder() matches exactly the your-... values that
.env.example ships instead of a pattern, so a token an operator chose
(a self-hosted Browserless TOKEN such as your-review-team-token) is
sent again. A test keeps the set equal to the template.
- make doctor mirrors Browserless: a configured token field is
authoritative, blank or not, and BROWSERLESS_TOKEN is read only when
the field is omitted. A blank or placeholder token now warns instead
of reporting the variable, cross-checked against the client.
- The keyless Jina and You.com providers stay ok but name JINA_API_KEY
or YDC_API_KEY when a placeholder there is ignored, and a literal
placeholder api_key in config.yaml is named when no env key backs it.
* fix(doctor): match each web provider's key precedence; drop the dead Jina api_key from docs
Found while re-checking #8206 against every provider's runtime:
- Browserless used as web_fetch had no doctor key check; it now gets the
same authoritative token handling as web_capture.
- InfoQuest reads only INFOQUEST_API_KEY, so doctor no longer counts an
api_key field in its tool config as configured.
- GroundRoute needs a key for every call but had no doctor entry, so a
missing key reported ok.
- You.com names a placeholder api_key in config that it ignores.
- The Jina web_fetch docs snippet carried api_key: $JINA_API_KEY, which
the tool never reads. With JINA_API_KEY now commented out in
.env.example, that line made config loading fail on a fresh install.
- The provider_credentials docstring now scopes doctor's use of the
predicate to web provider keys, and the scripts guide no longer says
.env.example ships live placeholder keys.
* feat(search): find conversations and jump to matching messages
* test(extensions): align example registry with search contribution
* test(search): scope bookmark search to its extension
* fix(search): address review correctness and scaling gaps
The ConversationScrollButton component existed but was never mounted,
so a conversation scrolled up had no way back to the latest messages.
Mount it inside the Conversation container with an accessible name from
the new common.scrollToBottom locale key, and lift it above the
subagent card wrapper's relative z-1 so a card scrolled under the
control cannot intercept its clicks.
Cover the stacking behavior with a Playwright test that wheels the list
up, asserts a card box paints across the button's center point, and
clicks through; the existing message-list DOM tests that mock the
conversation module stub the new import.
Co-authored-by: Nan Gao <thunderggnn@gmail.com>
In long-running agent workflows with repeated context compaction, empirical gateway ablations across 132 real sessions revealed that standard boundary compliance remains robust, while semantic drift can occur when model-generated summaries rephrase literal approval phrases and action conditions.
This change converges PR #8114 into a clean built-in summary prompt enhancement:
- Replaces regex extraction, tag elevation, and heuristic revocation with an extended built-in DeerFlow summary prompt (_DEERFLOW_SUMMARY_PROMPT).
- Instructs the summarizer to faithfully record active user requirements, literal approval phrases, permitted actions, and later explicit revisions without paraphrasing.
- Treats summary text strictly as historical data, fully preserving the security boundary and control/data plane separation of DurableContextMiddleware.
- Unifies automatic and manual compaction factory paths to select the built-in template when unconfigured, while preserving explicit summary_prompt precedence.
- Adds comprehensive sync/async unit tests covering built-in and custom templates.
Co-authored-by: Totoro <Totoro-qaq@users.noreply.github.com>
Co-authored-by: Totoro <msy626836554@gmail.com>
* docs: turn README into an index and move reference detail to docs/guide
The README had grown to 393 KB because every change appended its notes there. The text moves verbatim into 18 topic guides under docs/guide/; only headings are re-leveled and relative links re-pointed. The README keeps the introduction, a quick start and a guide index. Contributor test notes move to CONTRIBUTING.md.
* docs: send user docs to topic guides and budget the root README
The rule 'update README.md for user-facing changes' is what grew the README. Guidance now points to docs/guide/, and check_agent_guidance.py fails when the root README exceeds 16 KiB (AG003).
* chore: remove unreferenced screenshots and finished plan notes
pr-build/ holds two PR screenshots (3 MB) from #1986. plans/ holds the plan for the merged #4049. docs/SKILL_NAME_CONFLICT_FIX.md is a February change note. Nothing references them.
* docs: route always-loaded AGENTS.md files to scoped guides
* docs(agents): cut scoped guides to scope, verify, traps, decisions
Each AGENTS.md keeps four kinds of content: the scope of the directory, the
verification commands, traps a local test run does not show, and decisions the
code cannot state. Values, field lists, function inventories, test names, issue
numbers and history are deleted, because the code, the tests and git state them.
Search and fetch providers share one community guide. Four guides with no
remaining content are removed. The size check now allows 6 KiB per file (hard
limit 8 KiB) and 24 KiB per chain (hard limit 32 KiB).
* style: format frontend guide table and guidance test imports
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(frontend): copy share links on the current origin
The sidebar Share action rewrote the origin to the hosted demo
(https://deer-flow-v2.vercel.app) whenever the page ran on localhost or
127.0.0.1, which are the default make dev and Docker entry points. The
copied link never opened the thread, because the thread only exists on
the local deployment, and opening it sent the thread id (and a custom
agent name) to a host the operator does not control.
Always build the link from window.location.origin and pathOfThread().
* docs(changelog): reference #8126 for the share link origin fix
* fix(frontend): make chat stream server errors actionable
* fix(frontend): log chat stream server errors
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(capabilities): tidy the extension detail layout and narrow-screen overflow
The extension detail view led with a page-level reload button and sat the
back button beside it 3px off: the two flowed inline and aligned by
baseline, which the back button takes from its icon. It also kept a
directory search that filters nothing there, a heading close to the page
title, and status copy that read like body text. On narrow screens the tab
strip widened the whole page, leaving a horizontal scrollbar.
Lead with the back button and move reload to the opposite end, drop the
search from the detail view, and give the detail an icon, package name and
a status notice holding the installation guide. On the directory, reload
now sits beside the hint that explains it. The tab strip scrolls inside
itself and keeps the active tab in view, and reload collapses to an icon
below `sm` with its accessible name intact.
No copy changes.
* fix(capabilities): realign the tab strip on resize without moving the page
Review follow-up. The active tab was only realigned when the tab changed, so
a rotation or window resize could leave it clipped. Realign whenever the strip
resizes, and leave manual swipes alone.
Scroll only the strip. scrollIntoView would also move the page, pulling it back
up when a resize arrives after the user has scrolled past the strip.
Also hide the strip's scrollbar in browsers without scrollbar-width support,
and find the page scroller in the E2E test by computed style rather than by a
class name.
A background-task detail or list read issued before the user cancels can
resolve after the mutation's success handler and overwrite the confirmed
"cancelled" snapshot with the pre-cancel "working" one, so the expanded card
shows the task as active again (and polling resumes) until a later response
corrects it.
useCancelBackgroundTask now awaits cancelQueries for the thread's
["background-tasks", threadId] prefix before seeding the confirmed detail and
list entry, so a read that started before the cancel is dropped instead of
landing afterwards. The following list invalidation is unchanged, so a mounted
card still refreshes from the server. This is the same ordering contract as the
memory mutations fixed in #6959.
Regression: frontend/tests/unit/core/background-tasks/hooks.dom.test.tsx uses a
real QueryClient with deferred detail and list reads; it ends on "working"
without the fix and keeps "cancelled" with it.
Co-authored-by: REI-330 <173389910+REI-330@users.noreply.github.com>
Co-authored-by: AochenShen99 <sac1063467113@hotmail.com>
* feat(subagents): continue native batches and honor item dependencies
* docs: link native batch follow-up changelog entries
* test(migrations): pin isolated history-seed upgrade to 0050
* fix(subagents): preserve follow-up scope, blockers and fair scheduling
* test(memory): freeze capacity evaluation time
* fix(subagents): guard follow-up admission and retain run controls
* fix(subagents): enforce runs:cancel on tool cancellation of follow-up batches
* style(tests): format test_batch_followup_host with ruff
* fix(subagents): drop the admission checkpoint check and keep SDK cancel compatible
The checkpoint check inside run admission waited on the checkpoint saver's
lock while holding the SQLite write lock, which could make another chat's
checkpoint write fail. Remove it; dispatch still checks the conversation
before admission.
A follow-up waiting for an unanswered question now keeps its claim until the
lease expires instead of rereading the conversation every poll. Cancel passes
the caller's run authority only to submitters that take it, so existing SDK
submitters keep the two-argument call. Document that retrying a prerequisite
by hand does not requeue its blocked items, and list 0052 as the migration head.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
A chat that observes an active run again (remount, chat switch, rejoin)
started its Working… timer from the browser's current time, dropping the
time the run had already spent. useThreadStream now exposes the active
run's created_at from the runs list, and MessageList moves its start back
to it. It never moves the start forward, so a run submitted in this view
keeps its local start.
Fixes#7886
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* docs: replace dead pull/5224 links with squash-merge commit permalinks in subagents reference (en/zh)
The "Related changes" list in harness/subagents/reference.mdx cites PR #5224
("tool assembly off the event loop"), but the PR and its issue counterpart were
deleted by repository administrators, so both URLs return 404. The PR itself was
merged: squash commit 5727449 matches the cited description verbatim.
Same approach as #7689 (changelog deleted-PR references): keep the [#5224] text
and entry description, replace the URL with the squash-merge commit permalink.
1 line changed in each of the en and zh pages.
* test(deermem): keep eviction fixtures fresh
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(extension-api): add task identity fields to CompactionEvent (0.2.10)
CompactionEvent carried the transform, its hashes and its counts, but
nothing that names the task, run or thread the compaction happened in.
An observer that wants to relate a compaction to the run that produced it
had to match hashes across sessions, which is unreliable.
Append three optional fields, task_id, run_id and thread_id (str | None,
default None). Existing keyword and positional construction is unchanged,
so already-released extensions keep working. The host fills them in the
next commit.
Bump deerflow-extension-api to 0.2.10 and the harness's exact pin with it.
* fix(extensions): hand compaction observers the live task store and its identity
The summarization middleware already recovers the live task store from
the runtime to observe its summary system call, and the registry
allocates that store precisely because a compaction observer is
registered. notify_context_compacted() then discarded it and handed every
observer a detached store, with a docstring claiming there was no live
task.
- The lead run worker and the subagent executor now seed every task store
they allocate with the task's TaskInfo, not only when a lifecycle
contributor is registered; the lifecycle notification reuses the seeded
object. A subagent without a parent run_id still gets no TaskInfo.
- compact_state() and acompact_state() pass the store from
task_store_from_runtime() to _record_compaction().
- notify_context_compacted() takes an optional task_store. A live store
goes to the observers and fills the event's task_id/run_id/thread_id
from its TaskInfo; without one (manual /compact runs outside any run)
observers still receive a detached store and the ids stay None.
For a background subagent the store keeps its public scope id (the
provider tool_call_id) while TaskInfo.task_id stays the execution id, as
lifecycle contributors already see it; the event follows TaskInfo.
* docs(extensions): document compaction event identity and seeded TaskInfo
The extensions guide and the developer manual still said compaction
observers always receive a detached store because no task is live at the
call site. Describe what the host does now: lead runs and subagents with a
parent run seed every task store with TaskInfo, compaction observers get
the live store, and CompactionEvent's task_id/run_id/thread_id come from
it; only manual /compact falls back to a detached store with None ids.
Adds the 0.2.10 row to the contract version table (PR number pending).
* docs(changelog): note compaction observers receive the live task identity
* docs(extensions): list compaction observers among task-store triggers
* docs: reference #7831 for the compaction event identity change
* docs(extensions): backfill contract history rows 0.2.5 to 0.2.9
The version table jumped from 0.2.4 to 0.2.10 although 0.2.5-0.2.9
shipped. Add those rows, attributing names merged without a bump to
the first release that carried them (#5796 in 0.2.5, #6920 and #5645
in 0.2.8, #7729 in 0.2.10). The page intro now says it documents
0.2.4 plus the 0.2.10 CompactionEvent fields, and points to the
history for the rest.
* feat(knowledge): add optional ingestion and status center
* docs(knowledge): record upstream CI budget fix and regression results
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(harness): keep a cancelled batch cancelled when retrying a failed item
retry_item only kept a paused batch as it was, so retrying a failed item
of a cancelled batch reset the batch to queued and the worker claimed and
ran the item. A cancelled batch now rejects the retry (409), re-checked
under the write lock because SQLite ignores FOR UPDATE, and the workspace
no longer offers Retry there.
* docs(changelog): reference #7792 for the cancelled batch retry fix
* fix(citations): keep copied markdown references valid
formatCitationMarkdownReference pasted the source title and URL into
"[title](url)" verbatim. A title with a stray "[" or "]" ends the link text
early, and a URL with an unbalanced parenthesis ends the destination, so a
copied reference no longer rendered as a link. Escape brackets in the title and
wrap a parenthesized URL in the angle-bracket form.
* fix(citations): only angle-bracket a parenthesized URL
normalizeUrl already percent-encodes "<", ">" and whitespace, so including
them in the trigger could wrap such a URL in an angle-bracket destination that
CommonMark forbids, which is worse than the bare form. Parentheses are the only
reachable delimiter that breaks the bare destination.
* fix(persistence): normalize non-finite floats before every JSON column write
The shared ORM engine serialized JSON columns with the lenient json.dumps
default (allow_nan=True), and the sync agent-storage engine set no
json_serializer at all. A non-finite float reaching a JSON column — run
metadata merges caller-supplied values, goal_verdict and
token_usage_by_model carry model/provider numbers, and json.loads('1e400')
yields inf — was emitted as the bare NaN/Infinity token, which PostgreSQL
jsonb rejects as invalid JSON, failing the whole write with DataError.
SQLite silently accepts and round-trips the invalid token, so the failure
was postgres-only and environment-dependent.
Adds dumps_json_safe/json_safe_value to persistence/json_compat.py (same
None-normalization the canonical display serializer applies since #7670,
which explicitly does not cover persisted state) and wires it into both
engines' json_serializer. Regressions pin: nested non-finite floats
normalize everywhere, finite values/bools pass through unchanged, the
async engine kwargs and the sync agent engine (postgres only; SQLite takes
no json_serializer) both route through it.
* fix(persistence): normalize JSON across synchronous engines
* docs: keep frontend guidance within the CI size budget
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(runtime): add Queue and Steer for active Web runs
* style(frontend): format Queue and Steer controls
* fix(runtime): retain user Steer controls and order active inputs
* docs: add Chinese changelog entry for Queue and Steer
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(subagents): add thinking_enabled and reasoning_effort config for sub-agents (#4875)
(cherry picked from commit b1083728b23504cb0f1d1a463f8db70c5ec1c5a7)
* fix: resolve CI failures for subagent thinking config PR
- Fix UnboundLocalError in task_tool.py: initialize parent_thinking_enabled
and parent_reasoning_effort before the if runtime block
- Remove unused imports from test_subagent_thinking_config.py
(SimpleNamespace, pytest, resolve_subagent_model_name)
- Fix import sorting in subagents_config.py and test file
- Add reasoning_effort: None to executor test assertion
(cherry picked from commit 187851e307b871937e48bb67ae1b34b8b69bf92d)
* fix: address review comments - remove parent inheritance, supports_thinking clamp, Literal type
- Remove parent_thinking_enabled/parent_reasoning_effort from metadata
extraction in task_tool.py (backward compatible: subagents only think
when explicitly configured)
- Add supports_thinking clamp in _create_agent to prevent cross-model
mismatch errors
- Change SubagentConfig.reasoning_effort to Literal type
- Fix sandbox_state assignment that was accidentally removed
- Fix syntax errors in subagents_config.py (missing closing parens)
- Update all docstrings/comments from "inherit" to "no override"
- Simplify test helper and remove parent inheritance tests
(cherry picked from commit 74f9c659ef4a98bd1756109929ab0748fab82317)
* feat(subagents): adapt reasoning settings to current native workers
* fix(subagents): retain model context on reasoning rejection
* fix(subagents): preserve model-factory reasoning diagnostics
---------
Co-authored-by: Neon549 <neon549@users.noreply.github.com>
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
`ArtifactFileDetail` and `loadArtifactContentFromToolCall` re-parsed
`write-file:` URLs with `new URL` + `decodeURIComponent` after
`parseWriteFileArtifact` had already rejected malformed values. The panel
restores its selection from session storage and only type-checks it as a
string, so a stale or hand-edited `write-file:%E0%A4%A` (or
`write-file://[`) threw `URIError`/`TypeError` during render.
Export the guarded parser and read `path` / `tool_call_id` from one parsed
result in both places; a rejected URL degrades to the raw string or
`undefined` instead of taking the panel down.
Translated zh pages link to the English docs routes (/docs/...) but kept
localized Chinese anchor slugs, which do not exist on the English pages.
Replace each Chinese anchor with the matching English heading slug
(verified present in the target file): a-run-fails-after-a-config-edit,
forks-are-accepted-not-refused, placements, failure-isolation, runaway-guards.
Co-authored-by: leom1919 <leom1919@users.noreply.github.com>
* fix(wizard): replace retired Claude Sonnet 4 default with claude-sonnet-5-5
The setup wizard's Anthropic default model is now `claude-sonnet-5-5` instead of the retired `claude-sonnet-4-20250514`. A new per-model `model_omitted_config` field makes the wizard leave out the extended-thinking (`budget_tokens`) and thinking-disabled templates for that model only, since Sonnet 5.5 rejects them; the other Claude models are unchanged. The Anthropic examples in `config.example.yaml` and `configuration.mdx` (which used `claude-3-5-sonnet-20241022`) now use `claude-sonnet-5-5` without those settings, and the change also adds a regression test and a CHANGELOG entry.
Notice anthropic-retired-models-2026-10-27 about api.anthropic.com's API describes the change. It was read from a file and is not signed by the provider.
* Update CHANGELOG with PR reference #7585
Added a reference to pull request #7585 in the CHANGELOG.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(runtime): present outputs before a loop wrap-up and explain step-limit stops
Loop-detection warnings now ask the agent to present the files it already
created in /mnt/user-data/outputs before its final answer, when
present_files is available.
A run that exhausts recursion_limit now ends with stop_reason
recursion_capped and a plain message that lists outputs it created but
did not present. An active goal stands down without an evaluator call.
The /wait error envelope keeps the GraphRecursionError name, and IM
channels reply with the step-limit message instead of a generic failure.
* docs(changelog): note run-end delivery and step-limit stops (#7588)
* test(runtime): stop pinning LangGraph's step count in the step-limit test
The produced paths and the message already follow the observed model
calls, so the exact count only pinned LangGraph's per-step accounting.
Also note on the step-limit goal branch that its caller passes no
evaluator factory, so it has to stay ahead of the evaluator section.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(api): survive a malformed CSRF cookie
`readCsrfCookie` percent-decodes a cookie the gateway sets verbatim.
Cookies are shared by every app on the same host (ports included), so a
stray or truncated `%` escape from another local app, a proxy, or a stale
value makes `decodeURIComponent` throw `URIError` inside `fetch` and
`getCsrfHeaders` — every state-changing call fails with an exception
before the gateway can answer 403.
Fall back to the raw cookie value when decoding fails, matching
`getLocaleFromCookie` (#7553).
* test(api): exercise sibling CSRF cookies in the DOM
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(channels): keep IM binding codes on screen and report goal state in replies
Connecting Slack, Discord, Feishu, DingTalk, WeCom, Buzz or QQ now opens a
dialog that keeps the /connect command on screen with a copy button, its
expiry time and the binding status, instead of a toast that disappears.
The bots' /connect replies and the IM /goal replies now use the owner's
web UI language (else channel_connections.notification_locale), and /goal
reports the goal's state as the web goal bar does: in progress, why it
stopped, or that it was met.
* docs(changelog): note the IM binding dialog and localized IM replies (#7591)
* test(channels): check IM reply tables against every notification locale
The connect-reply check compared a fixed en-US/zh-CN pair, and the /goal
reply table had none. Both now match NOTIFICATION_LOCALES with the same
keys in each, like the existing _GOAL_REASON_TEXT check, so a new locale
fails here instead of raising KeyError at reply time.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>