mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
Related: #160668 (fixes the fitting-table case; renderer-valid continuations for tables longer than `maxChars` are out of scope here, so the issue stays open for that part)
## What Problem This Solves
Fixes: with block streaming on (Discord defaults: 800/1200, paragraph), a reply containing a Markdown table that fits in one chunk gets split mid-table, so the channel renders the first part as a code table and posts the remaining rows as raw `| ... |` Markdown.
## User Impact
Streamed replies with a table that fits the chunk limit now arrive with the whole table rendered consistently. Tables larger than `maxChars` still split at row boundaries.
## Why This Change Was Made
The block chunker already refuses to break inside fenced code, but it had no idea about tables: once the buffer passed `minChars`, the last newline inside a still-streaming table was a valid break. The chunker now treats a table that fits in `maxChars` the way it treats a fence when choosing a break:
- Table ranges come from the same markdown-it table grammar the renderers use (`findMarkdownTableRanges` in markdown-core), so single-dash delimiters, single-column tables, rows without edge pipes, quoted tables and tables inside code fences behave like they render.
- While streaming, a table stays open until a full line follows it, so the unfinished line after it (even a bare `> `) may still become a row. Whether a table fits is decided by its parsed rows only, so an unfinished heading after it can't cost it that protection. A trailing header line waiting for its delimiter row is kept whole too. Other lines with pipes aren't protected, so prose containing `|` still streams at its normal newline breaks.
- If the buffer reaches `maxChars` inside a table that fits, the chunker breaks before the table (below `minChars` if it has to), not inside it.
- A table longer than `maxChars` isn't protected, so it still splits at row boundaries as before. Its headerless continuation still renders raw, as on main.
- If a streaming table that starts the chunk exactly fills `maxChars`, the chunker waits only until the next line starts; the final flush also ends the wait. Once the next line starts, the table is emitted whole at the cap, or split at row boundaries if it has grown. The size check counts row trailing spaces, so no chunk can exceed `maxChars`.
Fences, paragraph and newline breaks outside tables, source-range accounting and the forced final flush are unchanged. `src/auto-reply/chunk.ts` now names `FenceSpan` directly because `findFenceSpanAt` is generic over start/end spans.
Earlier attempts (#66344, #66568, #66557, #73981) were closed unmerged. This one is different:
- It uses the renderer's own parser, not a new regex table detector. That covers the repeated review notes about `-{3,}` delimiters, single-column tables, rows without edge pipes and pipes inside code.
- It keeps a streaming table open until a blank line arrives, not until the current buffer tail, which was the P1 in #66344 and #66568.
- Breaks at the table's own edges stay allowed. Only breaks inside a table that fits are refused.
- There's no import cycle through `chunk.ts`, it targets the current `embedded-agent-block-chunker.ts`, and it doesn't add a second limit model: `maxChars` stays the only cap.
Thanks @giodl73-repo for the fresh reproduction.
## Evidence
**Real Gateway, Discord plugin against a Crabline mock of the Discord API, frozen base `
|
||
|---|---|---|
| .. | ||
| active-memory | ||
| model-providers | ||
| qa-e2e-automation | ||
| active-memory.md | ||
| agent-bindings.md | ||
| agent-loop.md | ||
| agent-runtimes.md | ||
| agent-workspace.md | ||
| agent.md | ||
| architecture.md | ||
| compaction.md | ||
| context-engine.md | ||
| context.md | ||
| decision-models.md | ||
| delegate-architecture.md | ||
| dreaming.md | ||
| experimental-features.md | ||
| features.md | ||
| main-session.md | ||
| managed-worktrees.md | ||
| mantis-slack-desktop-runbook.md | ||
| mantis.md | ||
| markdown-formatting.md | ||
| memory-architecture.md | ||
| memory-builtin.md | ||
| memory-honcho.md | ||
| memory-provenance.md | ||
| memory-search.md | ||
| memory.md | ||
| messages.md | ||
| model-failover.md | ||
| model-providers.md | ||
| models.md | ||
| multi-agent.md | ||
| multi-user.md | ||
| oauth.md | ||
| parallel-specialist-lanes.md | ||
| personal-agent-benchmark-pack.md | ||
| presence.md | ||
| progress-drafts.md | ||
| qa-e2e-automation.md | ||
| queue-steering.md | ||
| queue.md | ||
| retry.md | ||
| session-attachment.md | ||
| session-pruning.md | ||
| session-search.md | ||
| session-state.md | ||
| session-tool.md | ||
| session.md | ||
| soul.md | ||
| standing-intents.md | ||
| streaming.md | ||
| subagent-yield-handoff.md | ||
| system-prompt.md | ||
| timezone.md | ||
| typebox.md | ||
| typing-indicators.md | ||
| usage-tracking.md | ||
| user-model.md | ||