Concepts
Every front end — the Rust core, the npm package, the Go implementation and the CLI — takes the same inputs and produces the same output; they are held to the same test cases. This page covers what they share. The per-language pages list the exact names.
These docs track the main branch. Anything marked next release is not in the current release on npm, crates.io
and Go yet; it ships with the next one.
Channels
A channel is where the output goes. Its name is the same string everywhere.
| Name | Goes to | Limit |
|---|---|---|
telegram-html |
Telegram parse_mode: "HTML". Nine tags (b i u s code pre a blockquote tg-spoiler), no headings or tables — a table goes out as a monospace block. |
4,096 |
slack-markdown |
Slack markdown_text. Standard Markdown, which Slack converts itself; mdwire repairs, escapes a * meant as a character (\*) and splits. |
12,000 |
github-markdown |
GitHub comments and PR bodies (GFM). A ~ or < meant as a character is escaped, and bold that GFM would not close (before a Korean particle) is written as <strong>. |
65,536 |
notion-markdown |
Notion page body (Notion-flavored Markdown). Four heading levels; inline HTML Notion cannot draw is stripped, and an autolink becomes [url](url). |
65,536 (not measured) |
plain |
Plain text — every marker removed. The fallback. | 12,000 |
html |
An HTML fragment for the browser, safe to set as innerHTML: text is escaped, inline tags from the source keep no attributes, and only allowed schemes become links. |
none |
The limit is a character count on the rendered output. The sender can set its own (see
options). For example, plain sent to Telegram has to fit 4,096.
Options
| Option | Default | Meaning |
|---|---|---|
| limit | the channel’s limit | Characters per part. Values below 256 are raised to 256 — every part needs room to close and reopen markup. Streaming never splits, and neither does html. |
| html · line breaks | br |
A line break inside a block: <br>, or space to let the browser fold it (for prose wrapped at 80 columns). |
| html · images | link |
 as a link that loads nothing until clicked, or load for <img> (allowed schemes only). |
| html · schemes | http, https, mailto |
Schemes allowed in links and image URLs. Giving a list replaces the default; it does not add to it. |
The three html options apply only to the html channel.
The repair report
Rendering also counts what it had to do. The first five are repairs — how often the model broke its own formatting:
| Field | Counts |
|---|---|
| closed emphasis | Emphasis still open at the end of a block, closed (**impact scope). |
| closed fence | A code fence still open at the end of the document, closed. |
| reverted code span | A backtick run with no partner, kept as text. |
| dropped marker | A stray ** dropped (tail**, where text comes before it). |
| guessed pair | Emphasis paired by a guess rather than by the rules — an opener CommonMark would not open (값**(합계)**를) matched with a mirror-shaped closer on the same line. Math with ASCII on both ends (x**(y)**z) is left out. A non-zero count marks an answer worth a look. next release |
The other six are channel rewrites — what the channel needed changed before posting:
| Field | Counts |
|---|---|
| escaped char | Characters the channel would read as syntax, escaped (GitHub’s \~ \< \*; Slack’s and Notion’s \*). |
| tag emphasis | Emphasis the channel cannot read as markers, written another way — GitHub’s <strong>, or an invisible U+2060 inside Slack’s markers. |
| stripped HTML | Source HTML removed: tags the channel cannot draw, comments, <br> turned into a line break. |
| rewritten bullet | List markers rewritten (* • → - , Telegram’s - → • , 1) → 1.). |
| rewritten table | Tables rewritten (separator and cell spacing, or turned into a monospace block). |
| converted marker | Emphasis and links respelled — _italic_ → *italic*, __bold__ → **bold**, <url|text> → [text](url). Markdown channels only. |
The streaming contract
A streamer takes the output token by token and returns what is safe to send now.
- What
pushreturns is final. A later chunk never rewrites it, andfinishonly appends the tail. So the pieces concatenated equal a one-shot render, whatever the chunk size — unless the document is long enough to be split into parts. - It holds back only what it must: a prefix it cannot classify yet, a marker run at the end of a chunk, and the inside of emphasis that has not closed. A whole paragraph is never held.
- Streaming does not split by the limit. One exception: a Notion table over the limit goes out as several tables with the header repeated, and streaming does the same.
Two ways to send while streaming, depending on the channel:
| The channel… | Send | Example |
|---|---|---|
| rewrites the whole message | accumulated output plus preview() — what is still held, drawn as if the input ended here. After finish, skip the last edit when revised() is false. |
Telegram editMessageText, Slack chat.update, React |
| only appends | each push piece as is. Never preview(). |
Slack appendStream |
closeOpen() is the stricter sibling of preview(): it only closes block markup that is already out
(<blockquote>, <pre>), without drawing what is held. Neither changes the streamer’s state.