Summary
remote-opencode v1.5.3 silently stops updating the Discord stream message once the model's accumulated output grows past ~1700 characters, and waitForReady can give up on a still-starting opencode serve even though the server eventually comes up fine.
Both bugs are easily hit in normal use; the first one makes the bot look broken (initial message pinned, no progress, no final answer), the second one mostly masks the first.
Bug 1 — Invalid Form Body content[BASE_TYPE_MAX_LENGTH]: Must be 2000 or fewer in length.
Reproduction
- Configure any project + channel via the bot's setup flow.
- In a thread on that channel, send a prompt that produces a model response longer than ~1700 characters. (Easiest: ask for a long summary, a file walk-through, anything that triggers a multi-paragraph answer.) First prompt after the bot starts is the most reliable trigger, because of bug 2 below.
- Watch Discord: the bot posts
🌿 \staging` · 🤖 `default`\n📌 Prompt: …\n\n🚀 Starting OpenCode server...` and then never updates. OpenCode is running normally and finishes its work, but the Discord message stays frozen on the initial text.
Expected
Progress indicators while the model is generating, and a final answer that includes all of the model's output (even if it has to be split across multiple Discord messages).
Actual
Bot looks unresponsive. In the bot's terminal you see:
[opencode stdout] opencode server listening on http://127.0.0.1:14097
Failed to edit stream message: Invalid Form Body
content[BASE_TYPE_MAX_LENGTH]: Must be 2000 or fewer in length.
…repeated on every 1-second tick of setInterval in executionService.ts. The error is just console.error'd and swallowed, so the user has no way to tell what's happening.
Root cause
src/services/executionService.ts builds the edited message as:
`${contextHeader}\n📌 **Prompt**: ${prompt}\n\n${spinnerChar} **Running...**\n${newContent}`
where newContent comes from formatOutput(accumulatedText) (default maxLength = 1900). Once the model produces ≥~1700 chars the full string exceeds Discord's hard 2000-char per-message limit and streamMessage.edit() rejects with BASE_TYPE_MAX_LENGTH. The catch in updateStreamMessage only logs and returns false; the periodic interval ignores the return value, so the message stays stuck.
The final-response path has a weaker version of the same bug: formatOutputForMobile does return chunks, but the first chunk is concatenated with the same ${contextHeader}\n📌 **Prompt**: ${prompt}\n\n prefix and then edit()'d without length checking, so it can fail for exactly the same reason. The "send remaining chunks as new messages" fallback only runs when the edit already failed.
The initial channel.send for the 🚀 Starting OpenCode server... message has no length check at all; a user prompt >~1800 chars throws and the function silently returns.
Bug 2 — waitForReady misclassifies a slow cold-start as a failure
Reproduction
- Stop the bot (or kill any leftover
opencode serve).
- Start the bot fresh.
- Send the very first Discord prompt of the session.
Expected
The bot waits for opencode serve to finish loading (models, providers, etc.) and then continues.
Actual
With a non-trivial cold-start you get:
❌ OpenCode execution failed: Service at port 14097 failed to become ready within 30000ms. Check if 'opencode serve' is working correctly.
even though opencode serve is happily up a few seconds later (re-running the same prompt works fine). Two underlying problems:
fetch in waitForReady has no per-request timeout. A single slow request can swallow the whole 30s budget before the loop iterates again. (isOrphanedServerRunning and isServerResponding already do this correctly; waitForReady was just missed.)
- 30 s is too tight for a cold start. Once loading models or providers takes ~35 s the timeout fires even though the probe itself was healthy.
Proposed fix
I have a working PR open (#XX, branch fix/discord-2000-overflow) that:
- Adds a
splitForDiscordTemplate({ header, prompt, body, maxLength }) helper in src/utils/messageFormatter.ts that returns { prefixBody, overflowChunks } — a Discord-safe edit body plus any overflow as separate channel.send() chunks. splitIntoChunks is also exported so tests can hit it directly.
- Routes every
message.edit() in executionService.ts (initial send, periodic updates, final response, connection-error and OpenCode-error paths) through the helper, so no path can push past 2000 chars. The initial send additionally truncates the user prompt defensively and returns a clear error if channel.send itself rejects.
- Adds
signal: AbortSignal.timeout(5000) to waitForReady's probe fetch, bumps the default timeout from 30 s → 60 s, and updates the timeout error message to mention the cold-start possibility.
- Ships unit tests for
splitIntoChunks boundaries, splitForDiscordTemplate budget behaviour, the new signal on the fetch call, AbortSignal.timeout being treated as a retryable failure, and a 45 s cold-start succeeding under the new 60 s default.
All 183 tests in the existing suite pass after the change (52 in messageFormatter + serveManager).
Environment
remote-opencode 1.5.3 (npm)
- Node 22.14.0
- Discord.js 14.25.1
opencode serve 1.18.4 (from @opencode-ai/opencode upstream)
Summary
remote-opencodev1.5.3 silently stops updating the Discord stream message once the model's accumulated output grows past ~1700 characters, andwaitForReadycan give up on a still-startingopencode serveeven though the server eventually comes up fine.Both bugs are easily hit in normal use; the first one makes the bot look broken (initial message pinned, no progress, no final answer), the second one mostly masks the first.
Bug 1 —
Invalid Form Body content[BASE_TYPE_MAX_LENGTH]: Must be 2000 or fewer in length.Reproduction
🌿 \staging` · 🤖 `default`\n📌 Prompt: …\n\n🚀 Starting OpenCode server...` and then never updates. OpenCode is running normally and finishes its work, but the Discord message stays frozen on the initial text.Expected
Progress indicators while the model is generating, and a final answer that includes all of the model's output (even if it has to be split across multiple Discord messages).
Actual
Bot looks unresponsive. In the bot's terminal you see:
…repeated on every 1-second tick of
setIntervalinexecutionService.ts. The error is justconsole.error'd and swallowed, so the user has no way to tell what's happening.Root cause
src/services/executionService.tsbuilds the edited message as:`${contextHeader}\n📌 **Prompt**: ${prompt}\n\n${spinnerChar} **Running...**\n${newContent}`where
newContentcomes fromformatOutput(accumulatedText)(defaultmaxLength = 1900). Once the model produces ≥~1700 chars the full string exceeds Discord's hard 2000-char per-message limit andstreamMessage.edit()rejects withBASE_TYPE_MAX_LENGTH. The catch inupdateStreamMessageonly logs and returnsfalse; the periodic interval ignores the return value, so the message stays stuck.The final-response path has a weaker version of the same bug:
formatOutputForMobiledoes return chunks, but the first chunk is concatenated with the same${contextHeader}\n📌 **Prompt**: ${prompt}\n\nprefix and thenedit()'d without length checking, so it can fail for exactly the same reason. The "send remaining chunks as new messages" fallback only runs when the edit already failed.The initial
channel.sendfor the🚀 Starting OpenCode server...message has no length check at all; a user prompt >~1800 chars throws and the function silently returns.Bug 2 —
waitForReadymisclassifies a slow cold-start as a failureReproduction
opencode serve).Expected
The bot waits for
opencode serveto finish loading (models, providers, etc.) and then continues.Actual
With a non-trivial cold-start you get:
even though
opencode serveis happily up a few seconds later (re-running the same prompt works fine). Two underlying problems:fetchinwaitForReadyhas no per-request timeout. A single slow request can swallow the whole 30s budget before the loop iterates again. (isOrphanedServerRunningandisServerRespondingalready do this correctly;waitForReadywas just missed.)Proposed fix
I have a working PR open (#XX, branch
fix/discord-2000-overflow) that:splitForDiscordTemplate({ header, prompt, body, maxLength })helper insrc/utils/messageFormatter.tsthat returns{ prefixBody, overflowChunks }— a Discord-safe edit body plus any overflow as separatechannel.send()chunks.splitIntoChunksis also exported so tests can hit it directly.message.edit()inexecutionService.ts(initial send, periodic updates, final response, connection-error and OpenCode-error paths) through the helper, so no path can push past 2000 chars. The initial send additionally truncates the user prompt defensively and returns a clear error ifchannel.senditself rejects.signal: AbortSignal.timeout(5000)towaitForReady's probe fetch, bumps the default timeout from 30 s → 60 s, and updates the timeout error message to mention the cold-start possibility.splitIntoChunksboundaries,splitForDiscordTemplatebudget behaviour, the newsignalon the fetch call,AbortSignal.timeoutbeing treated as a retryable failure, and a 45 s cold-start succeeding under the new 60 s default.All 183 tests in the existing suite pass after the change (52 in
messageFormatter+serveManager).Environment
remote-opencode1.5.3 (npm)opencode serve1.18.4 (from@opencode-ai/opencodeupstream)