Skip to content

fix(kb): clamp markdown chunk overlap to the heading-reduced body budget - #10190

Open
iuiu-py wants to merge 3 commits into
AstrBotDevs:masterfrom
iuiu-py:fix/markdown-chunker-overlap-budget
Open

iuiu-py wants to merge 3 commits into
AstrBotDevs:masterfrom
iuiu-py:fix/markdown-chunker-overlap-budget

Conversation

@iuiu-py

@iuiu-py iuiu-py commented Sep 22, 2026 •

Copy link
Copy Markdown

Fixes #9998

What Changed

In MarkdownChunker._sections_to_chunks(), when a section's heading-path prefix reduces the body's effective chunk_size, the configured chunk_overlap is now clamped to that reduced budget (max(0, min(chunk_overlap, effective_chunk_size - 1))), and the effective budget itself is floored at 1. Sections without heading context and sections that fit in one chunk are unaffected.

Why

The chunker deducts the heading-prefix length from the body budget but passed the caller's chunk_overlap through untouched. The overlap is validated externally against the original chunk_size, so an externally legal pair — e.g. chunk_size=256, chunk_overlap=100 with a 200-char parent heading — became overlap(100) >= effective_chunk_size(64) inside the recursive chunker, which raises ValueError: chunk_overlap must be less than chunk_size and fails the whole knowledge-base upload at the chunking stage. The exact same input chunks fine with include_heading_context=False, which made the failure look config-independent.

Testing

  • tests/unit/test_markdown_chunker_overlap.py::test_long_heading_prefix_clamps_overlap_to_body_budget reproduces the issue verbatim ("# " + "A"*200 + "\n\n## Child\n" + "B"*600, 256/100): it raises on unpatched master (95e98b8) and produces body-bearing chunks with the fix.
  • The control case (include_heading_context=False) is pinned by the second test.
  • python -m pytest tests/unit/test_markdown_chunker_overlap.py -q → 2 passed; ruff format/check clean on both files.

Summary by Sourcery

Fix knowledge-base chunking and retrieval ranking edge cases, and produce valid streamed MiniMax WAV output.

New Features:

  • Repair streamed MiniMax WAV audio headers so generated files remain compatible with strict decoders.

Bug Fixes:

  • Prevent markdown chunking failures when heading context reduces the effective body budget below the configured overlap.
  • Ensure candidates returned by only one retrieval channel are not unfairly penalized during rank fusion.

Tests:

  • Add coverage for markdown overlap clamping, retrieval-channel-only ranking, and MiniMax WAV header repair.

wangzifei added 3 commits September 22, 2026 15:40
The weighted fusion counted a candidate that one channel did not
retrieve as scoring 0.0 in that channel. A missing result means the
channel never scored it, not that it scored worst, and the conflation
made the sparse channel unable to contribute any unique result: with
the default dense_weight=0.9 a sparse-only candidate could score at
most 0.1, below the dense channel's mid-field, so exact FTS5 matches
for rare tokens (e.g. case variants the embedding misses) fell out of
the fused top-k entirely.

Only weight channels that actually retrieved the candidate: the fused
score is the weight-normalized average over the channels that scored
it. Candidates retrieved by both channels keep their exact previous
scores, and dense preference when both channels disagree is unchanged.

Fixes AstrBotDevs#9868
With stream=True MiniMax returns a server-side ffmpeg streamed WAV whose
RIFF and data chunk sizes are 0xFFFFFFFF placeholders, because the total
length is unknowable while streaming. _audio_play concatenated the hex
chunks and get_audio wrote the bytes verbatim, so every saved .wav had
an invalid header. Browsers tolerate it, but strict decoders (Android
WebView, system players) reject the file with no error anywhere in the
AstrBot logs.

Walk the chunk list after assembly and rebuild the RIFF and data sizes
from the actual bytes; files whose header is already consistent, and
non-WAV payloads, pass through unchanged.

Fixes AstrBotDevs#9860
When a section inherits its heading-path prefix, the markdown chunker
deducts the prefix length from the body's chunk_size budget but passed
the caller's chunk_overlap through untouched. An externally valid pair
like chunk_size=256 / chunk_overlap=100 therefore became
overlap(100) >= effective_chunk_size(64) inside the recursive chunker
and raised ValueError, failing the whole upload at the chunking stage;
the same input chunks fine with include_heading_context=False.

Clamp the overlap to the reduced budget (and floor the budget at 1) so
legal external configurations keep working regardless of heading
length.

Fixes AstrBotDevs#9998

@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/provider/sources/minimax_tts_api_source.py" line_range="42-43" />
<code_context>
+        if csize > remaining:
+            break
+        pos = chunk_start + csize + (csize % 2)
+    if not fmt or not data_start or not data_size:
+        return audio
+    if (
+        struct.unpack_from("<I", audio, 4)[0] == len(audio) - 8
</code_context>
<issue_to_address>
**issue (bug_risk):** A valid zero-length WAV, or a truncated streamed WAV whose data chunk has no remaining payload, takes the early return and retains a stale `0xFFFFFFFF` data length instead of being repaired. `get_audio` accepts any nonempty returned bytes and writes that invalid header to disk.

**Triggers:** When the assembled stream has a `fmt ` chunk followed by a zero-byte `data` payload and an inconsistent data-size field.

**Suggested fix:** Distinguish absence of a `data` chunk from a present zero-length `data` chunk, and repair the latter even when its actual payload length is zero.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and the markdown change can produce different knowledge-base chunks and embeddings that remain stored after a revert, so correcting a wrong result may require reindexing, although the impact is bounded and repairable. The rank-fusion and WAV-header changes affect runtime retrieval or playback behavior and are otherwise reversible by reverting the PR.

Blocking findings: astrbot/core/provider/sources/minimax_tts_api_source.py:43


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

Comment on lines +42 to +43
if not fmt or not data_start or not data_size:
return audio

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.

issue (bug_risk): A valid zero-length WAV, or a truncated streamed WAV whose data chunk has no remaining payload, takes the early return and retains a stale 0xFFFFFFFF data length instead of being repaired. get_audio accepts any nonempty returned bytes and writes that invalid header to disk.

Triggers: When the assembled stream has a fmt chunk followed by a zero-byte data payload and an inconsistent data-size field.

Suggested fix: Distinguish absence of a data chunk from a present zero-length data chunk, and repair the latter even when its actual payload length is zero.

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.

[Bug] Markdown 长父标题压缩正文预算后,合法 chunk_overlap 导致分块失败

1 participant