Skip to content

feat(provider): support native video input modality - #10202

Open
JosephTian876 wants to merge 6 commits into
AstrBotDevs:masterfrom
JosephTian876:feat/add-video-input-modality
Open

JosephTian876 wants to merge 6 commits into
AstrBotDevs:masterfrom
JosephTian876:feat/add-video-input-modality

Conversation

@JosephTian876

@JosephTian876 JosephTian876 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Adds video as a first-class input modality, mirroring the existing image_urls /
audio_urls flow, so providers that declare it receive the video itself instead
of only a text placeholder.

Today _append_video_attachment() only appends:

[Video Attachment: name X.mp4, path C:\...\X.mp4]

so the model never sees any picture. On a real QQ deployment this pushed agentic
models into reading the file themselves (astrbot_execute_python + cv2 frame
extraction, ffprobe, web search for the dialogue), turning one video message
into ~30 tool calls and ~12 minutes before a reply.

Requested in #8048. Supersedes #9424, which implements the same approach but is
conflicting with master and misses the two failure modes described below.

Modifications / 改动点

Feature — video input modality

  • agent/message.py: add VideoURLPart and register video_url in the
    ContentPart type literal. Registration is required: the registry lookup in
    ContentPart.__get_pydantic_core_schema__ is unguarded, so an unregistered
    block raises KeyError as soon as it is validated from history.
  • provider/entities.py: add ProviderRequest.video_urls, count it in
    __repr__ / _print_friendly_context, and emit video_url blocks from
    assemble_context().
  • provider/sources/gemini_source.py: process video_url blocks into
    inline_data parts, in both _prepare_conversation and the provider-side
    assemble_context().
  • astr_main_agent.py: collect the resolved video path into req.video_urls
    alongside the existing text placeholder.
  • config/default.py: expose video as a model modality option.
  • agent/runners/tool_loop_agent_runner.py, provider/modalities.py: include
    video in modality gating and degradation.

Fix 1 — the Gemini dispatch no longer has a catch-all else

_prepare_conversation previously ended with:

if item["type"] == "text":
    ...
elif item["type"] == "image_url":
    ...
else:
    parts.append(process_audio_url(item["audio_url"]))

A video_url block fell into that else and raised KeyError: 'audio_url'.
_prepare_conversation is awaited outside the try: that guards the API call,
so the except APIError never saw it and the exception escaped text_chat.
Because the block is persisted into the session history, the failing message was
then replayed on every later request — one video would poison the whole session,
including plain-text turns. This is the same failure shape as #9295.

The branch is now exhaustive, and unknown block types degrade to a text
placeholder instead of raising.

Fix 2 — video blocks are marked transient

assemble_context() output is written into run_context.messages and replayed
on every subsequent request, so a 20 MB video became ~27 MB of base64 resent each
turn. The video block now carries _no_save, which
dump_messages_with_checkpoints filters out. Providers still receive it, because
they serialize messages with model_dump(), which does not emit _no_save.

Video is opt-in

An empty or missing modalities list keeps its existing "supports everything"
meaning for text/image/audio, but not for video. Video postdates that
convention and is not universally supported, so a provider that never opted in
would fail the entire request instead of degrading. Video is sent only when
video is explicitly present in modalities.

Fix 3 — video stays opt-in on the fallback path (review follow-up, 0274e3a)

sanitize_contexts_by_modalities treats empty or missing modalities as
"supports everything", and _sanitize_contexts_for_provider short-circuited on
unconfigured providers. But that function also sanitizes an already-assembled
context when a request falls back to another provider — a context the primary
provider may have built with a video_url block because it declared the video
modality. An unconfigured fallback therefore received the video and failed the
whole request instead of getting [Video].

Video is now degraded there too. The runner still hands back the original context
objects untouched when the provider is unconfigured and there is no video block
to strip, so nothing else about that path changes — entering the sanitizer
unconditionally converted every plain-text history to dicts, which broke six
existing test_tool_loop_agent_runner tests.

Fix 4 — a VideoURLPart in extra_user_content_parts behaves like video_urls (review follow-up, 0274e3a)

Such a part used to be appended verbatim: never resolved to a data URI, and never
marked transient, so a data-URI video stayed in the conversation history as a
multi-megabyte payload and a local path was replayed after its file was gone. It
now goes through the same MediaResolver(..., media_type="video") call and is
flagged _no_save, and a message left with nothing but transient blocks keeps a
[Video] text placeholder so it cannot persist as empty content.

Dashboard

  • ProviderModelsPanel.vue, providerMetadata.ts, useProviderSources.ts,
    ProviderPage.vue, ProviderChatCompletionPanel.vue: add a video capability
    badge. Unlike image/audio it is not reported as enabled for unconfigured
    providers, matching the opt-in behaviour above.

  • i18n/locales/{zh-CN,en-US,ja-JP,ru-RU}: add the models.metadata.video label.

  • Regenerated MDI subset assets (the icon set changed).

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

Verification steps

  1. uv sync --group dev
  2. uv run pytest tests/test_video_input_modality.py -v
  3. uv run ruff format --check . && uv run ruff check .
  4. uv run pytest tests/ -q
  5. cd dashboard && pnpm install --frozen-lockfile && pnpm run build

Results

tests/test_video_input_modality.py    23 passed
ruff format --check                   516 files already formatted
ruff check                            All checks passed!
tests/ (full suite)                   3844 passed, 68 skipped, 0 failed
dashboard: vue-tsc --noEmit           exit 0
dashboard: pnpm run build             (see build log)

Update: master synced, CI is green

master moved forward while this PR was open, and the merge surfaced two
things worth calling out.

1. The upstream failure that made Run pytest suite red is fixed upstream.
The earlier red result was not caused by this change: uv.lock is gitignored
and sqlmodel>=0.0.24 has no upper bound, so uv sync --dev re-resolves
SQLModel on every CI run, and SQLModel 0.0.45 (released 2026-09-21T21:51Z)
began rejecting naive datetimes — breaking test_backup, test_dashboard,
test_document_storage_fts, test_kb_upload_atomicity and test_stat_service
(11 failures, reproducible on that exact commit). Master now carries
28c6aef1e fix: preserve datetime storage across SQLModel upgrades (#10211),
so syncing master resolves all 11. The full suite is green on all three
platforms (see the CI comment on this PR).

2. The merge exposed a gap in this feature, now fixed.
VideoURLPart is new in this PR, but only the Gemini path was given an
explicit branch for it. OpenAI and Anthropic fall through to the pre-existing
catch-all else: raise ValueError(...) in their extra_user_content_parts
loop, so passing a VideoURLPart raised instead of degrading — contrary to the
opt-in/degrade contract this PR establishes. Those two providers now emit a
[Video] text placeholder, consistent with how they already handle
AudioURLPart. The catch-all is untouched, and ProviderOpenAIResponses
inherits the same method, so one branch covers both OpenAI adapters.

The only merge conflict was in the generated dashboard/src/assets/mdi-subset/
assets (icon font subsets rebuilt from src/ on every build). They were
regenerated from the merged sources rather than hand-merged — the resulting CSS
is the union of both sides plus mdi-video-outline, and three independent
regenerations produce byte-identical files. The new pnpm run i18n:check gate
added on master also passes, with models.metadata.video present in all four
locales.

The new tests cover, among others:

  • a video_url dict validating through the registry without KeyError
  • assemble_context() flagging the video block _no_save
  • the block reaching model_dump() (provider payload) while being absent from
    dump_messages_with_checkpoints() (persisted history)
  • _prepare_conversation accepting a video_url block and emitting an
    inline_data part with mime_type == "video/mp4"
  • an unknown block type degrading to text instead of raising
  • video being dropped for an unconfigured provider, sent when declared, and
    replaced with [Video] when not declared
  • video being degraded for an unconfigured provider on the fallback path, with
    the rest of the history copied through untouched
  • a VideoURLPart in extra_user_content_parts being resolved, marked
    transient, kept out of history, and dropped when it cannot be resolved

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
    (Commented on [Feature] 增加视频输入支持 #8048 and feat: wire up video input modality for MiniMax providers #9424 with the design and the two failure modes.)

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。

  • 📚 I checked the affected WebUI instructions and screenshots in docs/zh and docs/en against the changed navigation, page structure, and labels, and updated them in this PR (or explained why no documentation update is needed).
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zh 和 docs/en 的相关操作说明与截图(或说明无需更新文档的原因)。
    (Checked: the model modality checkbox list is not documented in docs/zh or docs/en — a search for 模态 / modalities / tool_use finds only unrelated provider and config references. The navigation, page structure and entry points are unchanged, so no documentation update is required.)

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txt 和 pyproject.toml 文件相应位置。
    (No new dependencies.)

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Support native video input as an explicitly enabled provider modality while safely degrading or excluding it for unsupported providers.

New Features:

  • Add native video input support across provider requests, Gemini payloads, model capability configuration, and dashboard capability indicators.

Bug Fixes:

  • Prevent video and unknown content blocks from crashing conversation replay or poisoning sessions when unsupported or malformed.
  • Keep video attachments transient so large payloads are delivered to providers without being persisted and resent in history.
  • Ensure unsupported and unconfigured providers, including fallback providers, degrade video input to a text placeholder.
  • Handle video content parts consistently across provider-specific extra content processing.

Enhancements:

  • Extend modality sanitization and request context handling to enforce video's explicit opt-in behavior while preserving existing defaults for other modalities.

Tests:

  • Add coverage for video validation, provider delivery, transient history handling, modality degradation, fallback behavior, and unresolved attachments.

Chores:

  • Add localized dashboard labeling and video capability icons.

Add ProviderRequest.video_urls so providers that declare the �ideo
modality receive the video itself instead of only a text placeholder.

Fixes two failure modes that a video block would otherwise introduce:

- gemini_source._prepare_conversation dispatched user content with a
  catch-all �lse that read item["audio_url"]. A video_url block raised
  KeyError outside the try that guards the API call, and because the
  block was persisted the error repeated on every later request. The
  branch is now exhaustive and unknown block types degrade to text.
- assemble_context output is replayed from history on every request, so
  video blocks are now flagged _no_save and dropped when the message is
  written back.

Video is opt-in: an empty modalities list keeps its existing "supports
everything" meaning for text/image/audio but not for video, so existing
configurations are unaffected.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/runners/tool_loop_agent_runner.py" line_range="344-352" />
<code_context>
     ) -> dict[str, T.Any]:
         modalities = self.provider.provider_config.get("modalities", None)
         if not modalities:  # Unconfigured (None or empty list) defaults to support all modalities for backward compatibility
-            return await request.assemble_context()
+            # Video is excluded from that default. It postdates the "supports
+            # everything" convention and is not universally supported, so a
+            # provider that never opted in would fail the whole request rather
+            # than degrade. Video is therefore strictly opt-in via the
+            # `modalities` list.
+            if not request.video_urls:
+                return await request.assemble_context()
+            return await replace(request, video_urls=[]).assemble_context()

         supports_image = "image" in modalities
</code_context>
<issue_to_address>
**issue (broader_impact):** A fallback provider with an empty or missing `modalities` list receives a native `video_url` from the primary provider's already-assembled context. `sanitize_contexts_by_modalities` treats empty modalities as supporting every modality, while the runner's new opt-in rule only applies when initially assembling the request, so an unconfigured fallback can receive video and fail instead of receiving `[Video]`.

**Triggers:** When the primary provider opts into video but a fallback provider is unconfigured or has an empty `modalities` list.

**Suggested fix:** Apply the same video-specific opt-in rule in `sanitize_contexts_by_modalities`, filtering `video_url` blocks to `[Video]` when `video` is absent even if modalities is empty.
</issue_to_address>

### Comment 2
<location path="astrbot/core/provider/entities.py" line_range="228" />
<code_context>
+            content_blocks.append({"type": "text", "text": "[Video]"})

         # 2. 额外的内容块(系统提醒、指令等)
         if self.extra_user_content_parts:
</code_context>
<issue_to_address>
**issue (bug_risk):** A `VideoURLPart` supplied through `extra_user_content_parts` is appended without resolving it to base64 and without setting `_no_save`. The block is therefore persisted in conversation history, so a data-URI video remains a large history payload and a local or temporary video path is replayed after its source file is gone.

**Triggers:** When callers provide video through `extra_user_content_parts` rather than `ProviderRequest.video_urls`.

**Suggested fix:** Resolve `VideoURLPart` entries like `video_urls`, mark the resulting block transient, and ensure the provider-facing serialization still retains it.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and video attachments are now resolved, base64-encoded, and transmitted to providers that declare video support, so an incorrect eligibility or routing decision could expose user media or send it outside the team. Reverting would stop future transmissions but cannot retract video data already sent.

Blocking findings: astrbot/core/agent/runners/tool_loop_agent_runner.py:352, astrbot/core/provider/entities.py:228


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/agent/runners/tool_loop_agent_runner.py
Comment thread astrbot/core/provider/entities.py
@JosephTian876

JosephTian876 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Update 2026-10-13 — this note is historical; the situation it describes is resolved.

master has since merged 28c6aef1e fix: preserve datetime storage across SQLModel upgrades (#10211), which fixes the SQLModel naive-datetime regression analysed below. This branch has been synced with master, so all 11 failures described here are gone and Run pytest suite is green on all three platforms on head cc28030f4.

The analysis below is kept as-is because it documents the root cause and the bisect that established this PR was not responsible. For the current CI state and the other changes made since, see the newer comment on this PR.


CI note for reviewers: Run pytest suite is red on this PR, but the failure is upstream and unrelated to this change. I bisected it.

Root cause: SQLModel 0.0.45 (released 2026-09-21T21:51Z)

uv.lock is listed in .gitignore (line 6) and pyproject.toml declares sqlmodel>=0.0.24 with no upper bound, so uv sync --dev in scripts/run_pytests_ci.sh re-resolves SQLModel from PyPI on every CI run. SQLModel 0.0.45 changed its DateTime bind handling to reject naive datetimes:

# sqlmodel/sql/sqltypes.py
def process_bind_param(self, value, dialect):
    if value is None:
        return None
    if value.utcoffset() is None:
        raise ValueError(
            "Datetime values must have timezone information. "
            "Use datetime.now(timezone.utc), or annotate the field with "
            "NaiveDatetime for naive storage."
        )

Its DateTime type is also no longer an instance of sqlalchemy.DateTime, so AstrBotImporter._convert_datetime_fields silently stops converting ISO strings.

Evidence

Master is still at 95e98b8 — the base of this PR, unchanged. That exact commit passed Run pytest suite on all three platforms in the master push run at 2026-09-21T08:27:11Z (head SHA 95e98b8a). The same code, with the same locked dependencies, fails today.

Bisect on this machine, running the failing tests with the venv interpreter (uv run re-syncs SQLModel back to latest, which silently invalidates a naive bisect):

sqlmodel result
0.0.44 45 passed
0.0.45 8 failed — identical to CI
0.0.46 8 failed — identical to CI

tests/test_backup.py::TestAstrBotImporter::test_convert_datetime_fields flips on the same boundary (pass → fail), as do both test_dashboard failures. Together that reproduces all 11 CI failures, on the same commit, flipping on a single dependency version.

Independently: every non-action_required Unit Tests run in the repository since 2026-09-21T21:51Z is red, and unrelated PRs fail the same job (fix/readme-relative-links, fix/9854-caption-animation-frames, fix/quoted-image-materialization).

This PR's own tests are green

tests/test_video_input_modality.py (23 tests) passes on all three platforms in the run above. Locally the whole suite is green against SQLModel 0.0.44: 3521 passed, 82 skipped, 0 failed.

The latest run on 0274e3a confirms the change adds no failures: the same 11 tests fail and the passed count moves from 3577 to 3587 — exactly the 10 tests this PR adds.

Everything else on this PR is green: format-check, CodeQL, Analyze (python), build, AstrBot Dashboard CI, and all 15 Smoke test combinations (3 OS × Python 3.10–3.14).

This job is not a merge gate

master has no required status checks — repos/AstrBotDevs/AstrBot/rules/branches/master returns only deletion, non_fast_forward and a pull_request rule whose sole requirement is required_approving_review_count: 1. So this red job does not block the merge; the PR is waiting on review only.

Fixing the underlying SQLModel regression is out of scope here (it needs uv.lock to be committed, or sqlmodel bounded, or the naive datetimes in the storage layer to be made tz-aware) and belongs in its own change against master.

Degrade `video_url` for an unconfigured provider on the fallback path.
`sanitize_contexts_by_modalities` treats empty or missing `modalities` as
"supports everything" for backward compatibility, but video is strictly
opt-in. Since that function also sanitizes an already-assembled context when
a request falls back to another provider, an unconfigured fallback could
receive a `video_url` block that the primary provider produced, and fail the
whole request instead of getting `[Video]`.

Handle a `VideoURLPart` passed through `extra_user_content_parts` the same way
as `video_urls`: resolve it to a portable data URI and flag it transient.
Without that the block was persisted verbatim, so a data-URI video stayed in
the conversation history as a multi-megabyte payload and a local path was
replayed after its file was gone. A message left with nothing but transient
blocks now keeps a text placeholder so it cannot persist as empty content.

The unconfigured provider keeps receiving its original context objects when
there is no video block to degrade, so the sanitizer is not entered and
nothing else about that path changes.
…t-modality

# Conflicts:
#	dashboard/src/assets/mdi-subset/materialdesignicons-subset.css
#	dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff
#	dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2
Providers without native video input fell through to the catch-all
�lse: raise ValueError(...) in their �xtra_user_content_parts loop,
so a VideoURLPart raised instead of degrading. Video is opt-in and
those APIs cannot accept it, so emit a [Video] text placeholder like
every other unsupported modality and never persist the payload.
@JosephTian876

Copy link
Copy Markdown
Contributor Author

CI is green — master synced and the review findings are addressed

Run pytest suite was previously red on this PR for an upstream reason (see my earlier note). That is now resolved, and the merge also surfaced one gap in this feature that I fixed.

Head: cc28030f4 · mergeable went from CONFLICTING to MERGEABLE

CI on cc28030f4

Check Result
Run pytest suite (ubuntu-latest) ✅ pass — 3m23s
Run pytest suite (macos-latest) ✅ pass — 5m0s
Run pytest suite (windows-latest) ✅ pass — 6m51s
format-check ✅ pass
build (Dashboard CI, incl. pnpm run i18n:check) ✅ pass
Analyze (python) / CodeQL ✅ pass
Smoke test — 15/15 (3 OS × Python 3.10–3.14) ✅ pass

windows-latest is continue-on-error in unit_tests.yml, so ubuntu/macos are the blocking lanes there.

What changed

1. Synced master. The earlier red Run pytest suite was not caused by this change: uv.lock is gitignored and sqlmodel>=0.0.24 has no upper bound, so uv sync --dev re-resolves SQLModel on every CI run, and SQLModel 0.0.45 started rejecting naive datetimes — breaking test_backup, test_dashboard, test_document_storage_fts, test_kb_upload_atomicity and test_stat_service (11 failures, reproducible on the base commit). master now carries 28c6aef1e fix: preserve datetime storage across SQLModel upgrades (#10211), which resolves all 11. The 11 upstream failures are gone; 0 failed.

2. Fixed a gap the merge surfaced (cc28030f4, 2 files, +16/−1). VideoURLPart is new in this PR, but only the Gemini path got an explicit branch for it. OpenAI and Anthropic fall through to the pre-existing catch-all else: raise ValueError(...) in their extra_user_content_parts loop, so a VideoURLPart raised instead of degrading — contrary to the opt-in/degrade contract this PR establishes. Both now emit a [Video] text placeholder, matching how they already handle AudioURLPart. Verified against the real provider classes:

Case Before After
OpenAI receives VideoURLPart ❌ ValueError ✅ [Video], no raise
Anthropic receives VideoURLPart ❌ ValueError ✅ [Video], no raise
Genuinely unknown part type raises ✅ still raises (catch-all intact)

ProviderOpenAIResponses inherits assemble_context from ProviderOpenAIOfficial (verified by identity), so one branch covers both OpenAI adapters.

3. Regenerated the MDI subset assets. The only merge conflict was in dashboard/src/assets/mdi-subset/ — icon-font subsets that subset-mdi-font.mjs rebuilds from src/ on every build, so both sides had produced different binaries. They were regenerated from the merged sources instead of hand-merged: the CSS is the union of both sides plus mdi-video-outline, and three independent regenerations are byte-identical. The new pnpm run i18n:check gate added on master passes too, with models.metadata.video present in all four locales.

Verification

tests/test_video_input_modality.py    23 passed
ruff format --check                   516 files already formatted   (uvx --from ruff==0.15.22)
ruff check                            All checks passed!
tests/ (full suite)                   3844 passed, 68 skipped, 0 failed
dashboard: pnpm run i18n:check        exit 0  (4131 leaf keys per locale, no drift)
dashboard: vue-tsc --noEmit           exit 0
dashboard: pnpm run build             exit 0

The local full-suite run deselects tests/test_local_shell_component.py and runs it separately: its child process gets an explicit stdin=PIPE (astrbot/core/computer/booters/local.py:376), which hangs under a non-TTY harness. 46 deselected == 31 passed + 15 skipped from that file, and 3844 + 68 + 46 = 3958 equals the collected total, so coverage is complete. That file's blob hash is identical across the PR head, the merge, and master — unrelated to this change.

The branch is a plain fast-forward over the previous head; nothing was force-pushed, and the pre-sync state is preserved at tag pr10202-presync-20261012.

@JosephTian876

Copy link
Copy Markdown
Contributor Author

Conflict with master resolved — CI is green

mergeable went from CONFLICTING back to MERGEABLE at head af8d696f4.

What actually conflicted: only the generated MDI icon subset, not the feature. master moved on while this branch was open and both sides regenerated dashboard/src/assets/mdi-subset/ independently, so the binary woff / woff2 blobs and the CSS count line could not be auto-merged. The Python and Vue changes merged cleanly.

How I resolved it: rather than picking one side's binary, I re-ran the generator on the merged tree:

cd dashboard && node scripts/subset-mdi-font.mjs

That yields 268 icons — master's 267 plus the one this feature needs, mdi-video-outline (\F0BDC). Regenerating twice produces byte-identical output, so the committed artifacts are reproducible from source, not hand-picked. dashboard/package.json still pins @mdi/font 7.2.96, unchanged by the merge.

Verification on the merged head (af8d696f4, with master at dffe92277 / v4.29.1):

  • tests/test_video_input_modality.py — 23 passed
  • tests/unit/test_astr_main_agent.py + tests/unit/test_agent_runner_config.py — 212 passed
  • full suite — 4557 passed, 62 skipped
  • ruff format --check and ruff check on all 10 changed Python files — clean
  • CI: every check green, including format-check, build, CodeQL and the three Run pytest suite jobs

No source file was changed to make the conflict go away; the diff against master is still the same 22 files as before the merge.

…t-modality

# Conflicts:
#	dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff
#	dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2
@JosephTian876

Copy link
Copy Markdown
Contributor Author

Recurring conflict fixed at the root — the generated font subset is no longer part of this PR

Merged latest master (e5e21b71a). mergeable is back to true at head d91eb29f5.

This is the second time this PR conflicted, and both times the conflict was only in the auto-generated MDI icon subset — never in the feature. That is a sign the PR was fighting an upstream artifact rather than a real change, so I fixed the cause instead of resolving the binaries again.

Why it keeps happening: dashboard/src/assets/mdi-subset/ holds three build outputs (materialdesignicons-subset.css, ...webfont-subset.woff, ...webfont-subset.woff2). They are regenerated from the icons used in dashboard/src by node scripts/subset-mdi-font.mjs, which runs from both the build/dev package scripts and a vite-plugin-mdi-subset hook. They are also listed in dashboard/.gitignore (src/assets/mdi-subset/), yet were force-added historically and now change whenever anyone upstream touches the dashboard. Any dashboard PR therefore conflicts on them eventually.

What changed here: this PR no longer ships those three files at all — the merge takes master's version, so the PR has zero diff on them and future icon churn upstream cannot conflict. 19 files instead of 22.

Verified the build heals itself: running node scripts/subset-mdi-font.mjs on the merged tree still emits .mdi-video-outline (the one icon this feature adds), so the capability badge renders without any committed artifact.

Verification on d91eb29f5:

  • tests/test_video_input_modality.py — 23 passed
  • tests/unit/test_astr_main_agent.py + tests/unit/test_agent_runner_config.py — 212 passed
  • ruff format --check + ruff check on all 10 changed Python files — clean

Note: this merge also pulled in opentelemetry-{api,sdk} from master (#10515), so the local venv needed a uv sync before the suite would import. CI is unaffected.

If you would rather keep these artifacts committed as a project convention, say so and I will restore them — but then this PR will conflict again on the next dashboard change.

@JosephTian876

Copy link
Copy Markdown
Contributor Author

CI red is upstream, not this PR — and a fix is already open as #10617

The three Run pytest suite jobs show fail, but they did not fail on an assertion: all three were cancelled at exactly 30m00s ("The job has exceeded the maximum execution time of 30m0s"), hanging at 77% of the suite.

Root cause — reproduces on pristine master, not just here:

tests/unit/test_core_lifecycle.py::TestAstrBotCoreLifecycleRestart::test_restart_terminates_managers_and_starts_thread hangs forever. 8774e53ff (#10515) added three calls to AstrBotCoreLifecycle.restart():

await shutdown_local_booter()
...
await asyncio.to_thread(close_trace_store)
if sp.db_helper is self.db:
    await sp.close()

The test mocks the managers and threading.Thread, but not asyncio.to_thread. Patching threading.Thread also affects the thread-pool workers to_thread schedules onto, so close_trace_store never gets a worker and the test blocks — I confirmed by faulthandler dump that it sits waiting on the executor while an apscheduler blocking-scheduler thread is alive.

Verification that this is not caused by this PR:

  • This PR touches neither core_lifecycle.py nor test_core_lifecycle.py — zero diff on both.
  • Checked out pristine origin/master (e5e21b71a) in a separate worktree: the same test hangs >90s there too.
  • Master's own CI is doing the same thing — unit_tests.yml on 1f1d3fb0 and 8774e53ff were both cancelled with the identical 30m timeout at the same 77%, and several other open PRs (fix/xinference-stt-test-errors, fix/qwen-tts-http-download-errors, …) are cancelled the same way.

Upstream fix already open: #10617 "fix: preserve trace snapshot ordering and stabilize cross-platform tests" — its description says "Fix the restart-test hang introduced after #10614", and it patches exactly this test to mock asyncio.to_thread and assert it was awaited with close_trace_store. Author reports 23/23 CI green at 02539ce38.

Everything else on this head is green: 20 of 23 checks pass, including format-check, build, CodeQL, all smoke tests, and Sourcery. Local verification on d91eb29f5:

  • tests/test_video_input_modality.py — 23 passed
  • tests/unit/test_astr_main_agent.py + test_agent_runner_config.py — 212 passed
  • ruff format --check + ruff check on all 10 changed Python files — clean

So this PR should go green once #10617 lands (or when master is otherwise fixed). No action from this PR is needed, and I did not add a second copy of that fix here to avoid colliding with #10617.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant