fix: degrade unresolvable voice attachments instead of failing the turn by JosephTian876 · Pull Request #10337 · AstrBotDevs/AstrBot · GitHub
Skip to content

fix: degrade unresolvable voice attachments instead of failing the turn - #10337

Merged
Soulter merged 1 commit into
AstrBotDevs:masterfrom
JosephTian876:fix/unavailable-quoted-media
Oct 3, 2026
Merged

Soulter merged 1 commit into
AstrBotDevs:masterfrom
JosephTian876:fix/unavailable-quoted-media

Conversation

@JosephTian876

@JosephTian876 JosephTian876 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Modifications / 改动点

Closes #10336

An unresolvable voice attachment aborts request assembly, and the raw exception text is sent to chat as the reply:

Error occurred while processing agent request: not a valid file:

Root cause (two layers).

  1. Record.convert_to_file_path() raises a bare Exception when file, url and path are all empty (astrbot/core/message/components.py:241), whereas Image.convert_to_file_path() raises ValueError. As a result is_recoverable_image_error() classifies the Record failure as non-recoverable (False), so the image branches' existing degrade pattern cannot be reused as-is — that shape would still re-raise and still kill the turn.

  2. Both Record branches in collect_initial_request (astrbot/core/astr_main_agent.py) call convert_to_file_path() without a try/except, while the sibling Image branches do guard it and degrade to TextPart("[Image unavailable]"). The exception escapes request assembly, is caught in agent_sub_stages/internal.py, and the raw text becomes the user-visible reply.

The trigger is broader than an empty voice. _materialize_media_ref → ensure_wav → convert_audio_format also raises a bare Exception on ffmpeg conversion failure and when ffmpeg is missing (astrbot/core/utils/media_utils.py:1552 / :1560). Corrupt audio, a missing ffmpeg binary, or an unsupported codec therefore all reach the same turn-killing path.

Changes.

  • astr_main_agent.py — degrade the two Record branches (current message and quoted chain) the way the sibling Image branches already do: log a warning and append a text placeholder instead of propagating. Because Record and the audio converter raise bare exceptions, these branches must degrade unconditionally rather than going through is_recoverable_image_error(). asyncio.CancelledError is a BaseException and still propagates.

  • agent_sub_stages/third_party.py — the Record and Image branches of the third-party request build have the same unguarded calls. That build sits upstream of the file's only try block, and pipeline/scheduler.py has a finally but no except (the event bus only logs), so a failure there loses the whole turn silently — strictly harder to diagnose than the local path's visible error. Guard both.

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

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

Five new unit tests in tests/unit/test_astr_main_agent.py and tests/unit/test_third_party_agent_sub_stage.py cover:

  • a resolvable voice keeps its local path (current message and quoted chain) — regression guard;
  • an unresolvable voice degrades with [Voice unavailable] instead of raising (both paths);
  • the third-party Record and Image branches degrade;
  • asyncio.CancelledError still propagates — regression guard.

Negative control (the two source files reverted to their unpatched state, all new tests kept): exactly the 3 degradation tests fail with not a valid file: / ValueError, and all pass with the patch. The other 2 tests are regression guards and correctly pass in both states.

pytest ./tests           -> 3594 passed, 82 skipped, 0 failed  (exit 0)
ruff format --check .    -> 514 files already formatted
ruff check .             -> All checks passed

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
  • 👀 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). For renamed, moved, or merged entry points, I included an old entry → new entry mapping in the documentation and changelog.
    / 我已对照变化后的 WebUI 入口、页面结构和术语,核对并在本 PR 中更新 docs/zh 和 docs/en 的相关操作说明与截图(或说明无需更新文档的原因)。入口改名、移动或合并时,已在文档和 changelog 中补充 旧入口 → 新入口 对照。
  • 🤓 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 文件相应位置。
  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

No new features, so the first item needs no prior discussion — this is a pure bug fix.
No WebUI entry points, page structure or terminology are touched, so no documentation update is required.
No new dependencies.

Summary by Sourcery

Gracefully degrade unavailable image and voice attachments so media resolution failures no longer interrupt agent turns.

Bug Fixes:

  • Prevent unresolvable voice attachments from aborting request assembly by degrading them to a [Voice unavailable] placeholder.
  • Prevent unavailable image and voice attachments in third-party requests from silently losing the turn.

Enhancements:

  • Preserve successful audio attachment handling and cancellation propagation while adding graceful media-failure handling.

Tests:

  • Add unit coverage for resolvable and unavailable voice attachments, third-party media degradation, and cancellation behavior.

An empty Record (no file/url/path) or a failed audio conversion raised a bare Exception from convert_to_file_path() and aborted request assembly. In the local agent path the raw exception text was then sent to chat as the reply, and in the third-party agent path the whole turn failed silently.

Degrade the same way images already do: log a warning and append a placeholder instead of propagating. The recoverability gate used by the image branches does not apply here because Record and the audio converter raise bare exceptions, not the OSError/ValueError family.

@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 1 issue

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

## Individual Comments

### Comment 1
<location path="astrbot/core/astr_main_agent.py" line_range="1529-1537" />
<code_context>
-                    audio_path = await comp.convert_to_file_path()
+                    try:
+                        audio_path = await comp.convert_to_file_path()
+                    except Exception as exc:  # noqa: BLE001
+                        logger.warning(
+                            "Voice attachment is unavailable (%s).",
+                            type(exc).__name__,
+                        )
+                        req.extra_user_content_parts.append(
+                            TextPart(text="[Voice unavailable]")
+                        )
+                        continue
                     req.audio_urls.append(audio_path)
                     _append_audio_attachment(req, audio_path)
</code_context>
<issue_to_address>
**issue (bug_risk):** The unconditional `except Exception` handlers also swallow non-recoverable failures from media resolution, such as `OSError` caused by resource exhaustion or unexpected programming errors, and silently continue without the attachment instead of propagating the failure. The existing image path explicitly rejects resource-exhaustion and programming errors through `is_recoverable_image_error()`, so the new voice and third-party image paths regress that safety boundary.

**Triggers:** When media conversion fails for a reason other than an invalid or unavailable attachment, such as an `OSError` from exhausted file descriptors or an unexpected implementation error.

**Suggested fix:** Classify recoverable media failures separately, or introduce a voice-specific recoverability predicate that preserves propagation for resource-exhaustion and programming errors while degrading ordinary resolution failures.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: astrbot/core/astr_main_agent.py:1537


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

Comment thread astrbot/core/astr_main_agent.py
@Soulter
Soulter merged commit a6265fa into AstrBotDevs:master Oct 3, 2026
23 checks passed
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.

[Bug] 引用语音时裸调用 convert_to_file_path 抛出异常,导致整轮失败并把错误文本发给用户

2 participants