fix(gateway): balance code fences on every remaining chunk-split path

Widen #48476's fence guarantees to the two splitters that still emitted
fence-broken chunks:

* GatewayStreamConsumer._split_text_chunks (fallback final send): close
  the orphaned ``` at each chunk boundary and reopen it — with the
  original language tag — on the next chunk, mirroring
  BasePlatformAdapter.truncate_message's contract.  Headroom is reserved
  so balanced chunks stay within the platform limit.
* Slack block_kit._split_text (3000-char section chunking): same
  close/reopen balancing for mrkdwn section text carrying fences.

With these, every chunk boundary — non-streaming send
(truncate_message), streaming overflow (_truncate_for_stream via
adapter.truncate_message per #45938), fallback final
(_split_text_chunks), final-send balance (ensure_closed_code_fences),
and Block Kit section splits — delivers fence-balanced chunks.

Regression tests probe each path with fenced fixtures, assert per-chunk
balance, limit compliance, language-tag reopening, and prose passthrough.
This commit is contained in:
Teknium 2026-07-23 08:21:23 -07:00
parent c934c533db
commit eaa35ae68f
4 changed files with 176 additions and 8 deletions

View file

@ -536,19 +536,40 @@ def render_blocks(
def _split_text(text: str, limit: int) -> List[str]:
"""Split ``text`` into <= ``limit``-char chunks on line, then hard, boundaries."""
"""Split ``text`` into <= ``limit``-char chunks on line, then hard, boundaries.
Chunks are fence-balanced: when a split lands inside a ``` code span that
survived into section text (the renderer normally routes fenced blocks to
``rich_text_preformatted``, but mrkdwn text can still carry fences), the
fence is closed at the end of the chunk and reopened on the next so each
section renders correctly on its own.
"""
if len(text) <= limit:
return [text]
# Reserve headroom for the close/reopen markers the balancing pass adds.
split_limit = max(limit - 8, limit // 2, 1) if "```" in text else limit
out: List[str] = []
remaining = text
while len(remaining) > limit:
cut = remaining.rfind("\n", 0, limit)
while len(remaining) > split_limit:
cut = remaining.rfind("\n", 0, split_limit)
if cut <= 0:
cut = limit
cut = split_limit
out.append(remaining[:cut])
remaining = remaining[cut:].lstrip("\n")
if remaining:
out.append(remaining)
if len(out) > 1 and "```" in text:
balanced: List[str] = []
reopen = False
for chunk in out:
if reopen:
chunk = "```\n" + chunk
odd = chunk.count("```") % 2 == 1
if odd:
chunk += "\n```"
reopen = odd
balanced.append(chunk)
out = balanced
return out