Skip to content

fix(discord): long code-fence replies can exceed the message length limit#110148

Merged
steipete merged 8 commits into
openclaw:mainfrom
Yigtwxx:fix/discord-chunk-fence-reopen-overflow
Jul 18, 2026
Merged

fix(discord): long code-fence replies can exceed the message length limit#110148
steipete merged 8 commits into
openclaw:mainfrom
Yigtwxx:fix/discord-chunk-fence-reopen-overflow

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where a Discord agent reply containing a fenced code block could be split into a message that exceeds Discord's 2000-character limit, so Discord rejects the send with HTTP 400 and the reply is dropped. It happens when the code block's opening fence line is long (for example a long or garbled info/language string): the chunker reopens the block on every continuation message with the full opening line.

Why This Change Was Made

chunkDiscordText keeps fenced blocks balanced by reopening the fence at the start of each continuation chunk, but the reserve accounting only budgeted the closing marker — never the reopened opening line. The fix reopens continuation chunks with a bare fence marker when the full opening line would not leave room for the closing marker plus body, and sizes split segments against the reopen prefix, so no emitted chunk can exceed maxChars. The first chunk still carries the language for highlighting; only continuation chunks (which are separate Discord messages) degrade. This is the opening/reopen-side counterpart to the existing closing-fence overflow guard in the same file.

User Impact

Long code-fence replies are now always split into messages that stay within Discord's limit and send successfully, instead of failing silently. No behavior change for normal-length fences.

Evidence

Real behavior proof (runtime output)

Rerun on the exact current head 5e9eb5fa29a, which includes both of @steipete's follow-ups
(preserve code after fence reopen and preserve limits when fences cannot fit), so this transcript covers both what the PR originally fixed (over-limit payloads) and what
that commit added (the body after a reopened fence staying on its own line).

Direct runtime invocation of the shipped public API (chunkDiscordTextWithMode) against
maxChars: 2000 (Discord's message limit). Input: a fenced block whose opening line is 1995 chars
— long enough that repeating it verbatim leaves no room for a closing fence — followed by a 40-line
body. Pre-PR source is materialized with git show <merge-base>:extensions/discord/src/chunk.ts, so
both runs execute the same harness against real code.

$ node --version
v22.20.0

[pre-PR base]
  chunks emitted              : 1534
  max chunk length            : 2001   (limit 2000)
  chunks over limit           : 41     -> rejected by Discord (HTTP 400)
  fence lines with fused code : 1492
  body preserved exactly      : false  (expected 1290 body chars, got 41)

[current head 5e9eb5fa29a]
  chunks emitted              : 4
  max chunk length            : 1999   (limit 2000)
  chunks over limit           : 0      -> all sendable
  fence lines with fused code : 0
  body preserved exactly      : true

Two independent failures on base, both fixed on head:

  1. Over-limit payloads. 41 chunks exceed 2000 chars, so Discord rejects them outright.
  2. Body loss. Only 41 of 1290 body characters survive, because code is fused onto the reopened
    fence line where Discord parses it as the info string rather than as content. This is precisely
    the case 02da4bf28c6 repaired; on head every reopened fence line is either the original opening
    line or a bare marker, and the body round-trips exactly.

The 1534-vs-4 chunk count is the same root cause: re-prepending a 1995-char opening line after every
flush leaves only a few characters per message for actual code.

Harness (dropped after the run)
import { chunkDiscordTextWithMode } from "./extensions/discord/src/chunk.js";

const LIMIT = 2000;
const openLine = "```typescript title=" + "x".repeat(1975);
const codeBody = Array.from(
  { length: 40 },
  (_, i) => `const value${i} = compute(${i}); // line ${i}`,
).join("\n");
const input = `${openLine}\n${codeBody}\n\`\`\``;

const chunks = chunkDiscordTextWithMode(input, { maxChars: LIMIT });
const over = chunks.filter((c) => c.length > LIMIT);
const max = chunks.reduce((m, c) => Math.max(m, c.length), 0);

// A reopened fence line must be the original opening line or a bare marker; anything else
// means code was fused onto the fence line, where Discord reads it as the info string.
let fused = 0;
for (const chunk of chunks) {
  const first = chunk.split("\n", 1)[0] ?? "";
  if (!first.startsWith("```")) continue;
  if (first !== openLine && !/^```[a-zA-Z0-9_+-]*$/.test(first)) fused += 1;
}

// Drop fence lines and compare whitespace-stripped bodies, so a chunk boundary splitting a
// line does not register as loss.
const emitted = chunks.flatMap((c) => c.split("\n")).filter((l) => !l.startsWith("```")).join("");
const normalize = (s: string) => s.replace(/\s+/g, "");
console.log(chunks.length, max, over.length, fused, normalize(emitted) === normalize(codeBody));

Run: ./node_modules/.bin/tsx chunk-fence.proof.mts, once on the head and once with
git show $(git merge-base HEAD origin/main):extensions/discord/src/chunk.ts written over the
source file.

Tests and checks

  • Reproduced on current main: chunkDiscordText(" + "" + `" + "a".repeat(2100) + "\n<body>\n` + "" + ", { maxChars: 2000 }) emits chunks of 2108–2109 chars (> 2000).
  • Added two focused tests (mirroring the existing closing-fence maxChars test) that fail on main and pass with this change; the full extensions/discord/src/chunk.test.ts suite passes (19/19).
  • A temporary fuzz sweep over maxChars ∈ {20,50,120,500,2000}, marker widths, opening-line lengths, and body sizes confirmed no chunk exceeds maxChars (removed before commit).
  • tsgo (extensions + extension tests), oxfmt, oxlint, and the max-lines ratchet are all clean.

Allow edits from maintainers is enabled.

When a chunk is flushed mid code-fence, the next chunk reopens the fence
with the full original opening line (info string included), but the
reserve accounting only budgets the closing marker. A long fence opening
line therefore pushed reopened continuation chunks past maxChars, which
Discord rejects with HTTP 400.

Reopen continuation chunks with a bare marker when the full opening line
would not leave room for the close and body, and size split segments
against the reopen prefix so no chunk exceeds maxChars. This mirrors the
existing closing-fence-overflow guard on the opening/reopen side.
@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord size: XS labels Jul 17, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 17, 2026
@clawsweeper

clawsweeper Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 18, 2026, 5:54 PM ET / 21:54 UTC.

Summary
The branch changes Discord fenced-code chunking so continuation messages use a fitting fence prefix or omit synthetic balancing fences when they cannot fit within the configured message limit, with regression coverage for character and line limits.

PR surface: Source +37, Tests +64. Total +101 across 2 files.

Reproducibility: yes. from source and supplied exact-head runtime evidence: a fenced reply with an extremely long opening line causes current-main continuation chunks to exceed the 2,000-character limit, while the PR head keeps emitted chunks within the limit and preserves the body.

Review metrics: none identified.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Refresh the merge/review result against current main before landing.

Risk before merge

  • [P1] The PR is behind current main; refresh the merge/review result against the current base before landing.
  • [P1] For pathological fence lines that cannot be balanced within maxChars, continuation output intentionally favors Discord’s hard limit over synthetic fenced-code rendering, so the assigned owner should confirm that tradeoff remains acceptable.

Maintainer options:

  1. Refresh and merge the bounded fix (recommended)
    Rebase or refresh this cleanly mergeable branch against current main, retain the exact-head fence-limit regression coverage, and merge if the owner accepts the hard-limit-first rendering tradeoff.
  2. Pause for a rendering-policy change
    Keep the PR open only if maintainers want a different user-visible policy for fences that physically cannot be both balanced and within the Discord limit.

Next step before merge

  • [P2] No mechanical repair is indicated: the remaining action is normal owner review and a current-base refresh, not a ClawSweeper fix lane.

Security
Cleared: The two-file TypeScript and test diff does not add dependencies, permissions, workflow changes, secret handling, or external code execution.

Review details

Best possible solution:

Rebase or refresh the clean merge result against current main, then land the focused fix if the Discord owner accepts the documented hard-limit-first behavior for impossible-to-balance fences.

Do we have a high-confidence way to reproduce the issue?

Yes from source and supplied exact-head runtime evidence: a fenced reply with an extremely long opening line causes current-main continuation chunks to exceed the 2,000-character limit, while the PR head keeps emitted chunks within the limit and preserves the body.

Is this the best way to solve the issue?

Yes, subject to the owner’s normal review of the extreme-input rendering tradeoff: accounting for the continuation reopen prefix at the chunker boundary is narrower and more maintainable than adding Discord-send retries or post-hoc truncation.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 87de81a5fd4a.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides an exact-head, direct runtime transcript against the shipped chunking API that compares pre-fix output with the fixed result and measures the hard message-limit and body-preservation outcomes.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body provides an exact-head, direct runtime transcript against the shipped chunking API that compares pre-fix output with the fixed result and measures the hard message-limit and body-preservation outcomes.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P2: This fixes a scoped Discord reply-delivery failure for unusually long fenced-code responses.
  • merge-risk: 🚨 message-delivery: The changed chunking and fence-reopen rules directly determine whether Discord accepts, renders, or drops long replies.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body provides an exact-head, direct runtime transcript against the shipped chunking API that compares pre-fix output with the fixed result and measures the hard message-limit and body-preservation outcomes.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides an exact-head, direct runtime transcript against the shipped chunking API that compares pre-fix output with the fixed result and measures the hard message-limit and body-preservation outcomes.
Evidence reviewed

PR surface:

Source +37, Tests +64. Total +101 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 48 11 +37
Tests 1 64 0 +64
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 112 11 +101

What I checked:

  • Exact-head behavior proof: The PR body reports a direct chunkDiscordTextWithMode runtime comparison at head 5e9eb5fa29ab6ce0c43c33d3d9cf4c60bffac1b4: the long-opening-fence scenario drops from 41 chunks over the 2,000-character limit and lost body content to zero over-limit chunks with exact body preservation. (extensions/discord/src/chunk.ts:57, 5e9eb5fa29ab)
  • Focused regression coverage: The PR changes extensions/discord/src/chunk.test.ts alongside the implementation, including boundary cases for very long opening fence lines and small maxChars values; the supplied evidence reports the focused suite passing on the exact head. (extensions/discord/src/chunk.test.ts:108, 5e9eb5fa29ab)
  • Owner-directed follow-up history: The PR’s follow-up commits 02da4bf28c6c414e4b3511c6bb98706ad9d376ad and b94133eff0b7631b7fd42d982b6eb5a528c7538e address preserving code after reopen and preserving limits when fences cannot fit; the PR timeline also records assignment to steipete. (extensions/discord/src/chunk.ts:57, b94133eff0b7)
  • Current-main status: The provided PR state reports a clean merge result but behind base state; no supplied evidence establishes that current main SHA 87de81a5fd4a2d4e3ef8bff9af22a3ed2ab8b4e8 already contains the fix, so an implemented-on-main close is not supported. (extensions/discord/src/chunk.ts:57, 87de81a5fd4a)

Likely related people:

  • steipete: Authored the two substantive follow-up commits in the affected chunking path and is assigned to the PR in the supplied timeline. (role: recent area contributor and assigned reviewer; confidence: high; commits: 02da4bf28c6c, b94133eff0b7; files: extensions/discord/src/chunk.ts, extensions/discord/src/chunk.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (9 earlier review cycles; latest 8 shown)
  • reviewed 2026-07-18T07:04:32.328Z sha 7b85de8 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T10:59:54.182Z sha 2550302 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T20:13:23.574Z sha 995986d :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T20:53:59.350Z sha 02da4bf :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-18T21:19:43.212Z sha 02da4bf :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T21:27:34.400Z sha b94133e :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-18T21:35:57.714Z sha b84dcb5 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-18T21:44:30.485Z sha 5e9eb5f :: needs real behavior proof before merge. :: none

@Yigtwxx

Yigtwxx commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Real behavior proof (runtime output)

Per the ClawSweeper ask: direct runtime invocation of the shipped public API (chunkDiscordTextWithMode) on current main vs this PR head (00fa2be), printing every emitted chunk's length against maxChars: 2000 (Discord's message limit).

Input: a fenced block whose opening line is long (``` + 2100 chars of info string) followed by a 40-line body — i.e. the garbled/long info-string case from the PR description.

$ node --version
v22.20.0
$ npx tsx extensions/discord/src/chunk.proof.mts

[current main]
  emitted chunks : 222
  lengths        : 1996, 261, 2117, 2109, 2108, 2108, ...
  max length     : 2117 (limit 2000)
  over limit     : 220 chunk(s) -> rejected by Discord (HTTP 400)
  fence-reopened : 221 chunk(s)

[PR head]
  emitted chunks : 4
  lengths        : 1992, 265, 157, 107
  max length     : 1992 (limit 2000)
  over limit     : 0 chunk(s) -> all sendable
  fence-reopened : 3 chunk(s)

On main, 220 of 222 emitted chunks exceed the limit (up to 2117 chars) because each continuation message re-prepends the full 2103-char opening line — every one of those sends is rejected. On PR head no chunk exceeds the limit (max 1992), and the block still reopens correctly on continuation messages so the code block keeps rendering.

Harness used (temporary, not committed)
// extensions/discord/src/chunk.proof.mts
import { chunkDiscordTextWithMode as head } from "./chunk.ts";
import { chunkDiscordTextWithMode as main } from "./chunk.mainproof.ts"; // git show HEAD~1:extensions/discord/src/chunk.ts

const MAX = 2000;
const fence = "```";
const text = `${fence}${"a".repeat(2100)}\n${"body line\n".repeat(40)}${fence}`;

function report(label, fn) {
  const chunks = fn(text, { maxChars: MAX });
  const lens = chunks.map((c) => c.length);
  const over = lens.filter((n) => n > MAX);
  console.log(`[${label}]`);
  console.log(`  emitted chunks : ${chunks.length}`);
  console.log(`  lengths        : ${lens.slice(0, 6).join(", ")}${lens.length > 6 ? ", ..." : ""}`);
  console.log(`  max length     : ${Math.max(...lens)} (limit ${MAX})`);
  console.log(`  over limit     : ${over.length} chunk(s)`);
  console.log(`  fence-reopened : ${chunks.filter((c) => c.startsWith(fence)).length} chunk(s)\n`);
}

report("current main", main);
report("PR head", head);

Both temp files were deleted after the run; the working tree is clean at 00fa2be.

Committed suite on this head is green as well:

$ node scripts/run-vitest.mjs run extensions/discord/src/chunk.test.ts
 Test Files  1 passed (1)
      Tests  19 passed (19)

Note on CI: the single red job (checks-node-compact-small-7test/scripts/lint-suppressions.test.ts) fails on an unrelated allowlist entry (extensions/reef/src/transport.ts|no-useless-assignment) that no longer exists in the tree — nothing in this PR touches lint suppressions or that extension. Happy to rebase if that helps clear it.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 18, 2026
@steipete steipete self-assigned this Jul 18, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 18, 2026
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 18, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 18, 2026
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 18, 2026
@steipete
steipete merged commit 01c6479 into openclaw:main Jul 18, 2026
156 of 172 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

@Yigtwxx
Yigtwxx deleted the fix/discord-chunk-fence-reopen-overflow branch July 18, 2026 22:07
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 19, 2026
…imit (openclaw#110148)

* fix(discord): reopening a long code fence overflows maxChars

When a chunk is flushed mid code-fence, the next chunk reopens the fence
with the full original opening line (info string included), but the
reserve accounting only budgets the closing marker. A long fence opening
line therefore pushed reopened continuation chunks past maxChars, which
Discord rejects with HTTP 400.

Reopen continuation chunks with a bare marker when the full opening line
would not leave room for the close and body, and size split segments
against the reopen prefix so no chunk exceeds maxChars. This mirrors the
existing closing-fence-overflow guard on the opening/reopen side.

* fix(discord): preserve code after fence reopen

Co-authored-by: Yigtwxx <[email protected]>

* fix(discord): preserve limits when fences cannot fit

Co-authored-by: Yigtwxx <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants