Skip to content

Stream message stays frozen + Service at port X failed to become ready within 30000ms on cold start #64

Description

@moyarzun

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

  1. Configure any project + channel via the bot's setup flow.
  2. 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.
  3. 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

  1. Stop the bot (or kill any leftover opencode serve).
  2. Start the bot fresh.
  3. 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:

  1. 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.)
  2. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions