anomalyco / anomalyco/opencode

Request assembly is O(N) sequential DB round-trips (N+1 per-message parts query): ~20 min before first token on a 6,400-message session

Open
#49,329 0 comments 0 reactions 1 assignee View on GitHub

@rekram1-node is already working on this.

Since Sep 16, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description


title: "Request assembly is O(N) sequential DB round-trips (N+1 per-message parts query): ~20 min before first token on a 6,400-message session"

Summary

On a long-lived session (6,397 messages / 24,215 parts / 41 MB of part data), model turn spends ~20 minutes assembling the request before any HTTP traffic is issued. The user sees "Thinking" with zero output and zero server activity. The root cause is an N+1 loading pattern in session message streaming: messages are paged 50 at a time, and each message triggers one awaited parts4(messageID) query — ~6,525 sequential awaited round-trips per turn, dominated by per-await runtime overhead rather than actual I/O.

Observed behaviour

Real session (agent work spanning ~1 week, single session, opencode desktop 1.18.29):

  • User prompt at 10:33:01 UTC. First assistant output token at 10:54:47 UTC — 21 min 46 s gap with no streamed tokens of any kind (including reasoning, which streams normally once generation starts).
  • vLLM server metrics during the gap: num_requests_running mostly 0, GPU 0%, request_prompt_tokens_count advanced by only a few hundred tokens (small auxiliary calls only). The 121K-token request finally hit the server at 10:54.
  • The session DB state at the time: 6,397 messages, 24,215 parts, 41.0 MB of part JSON.

After starting a fresh session in the same workspace, turns are back to 1-3 s.

Root cause (from the 1.18.29 desktop bundle, identical in the CLI binary)

session service, stream9(sessionID):

function stream9(sessionID) {
  const size16 = 50;                  // page size
  return Effect.gen(function* () {
    let before;
    while (true) {
      const next3 = yield* page({ sessionID, limit: size16, before })...
      for (const row of next3.items) {
        ...
        parts: yield* parts4(input.messageID)   // one awaited query PER message
      }
    }
  })
}

toModelMessagesEffect is then invoked over the result. So per turn:

  • 6,397 / 50 = 128 page queries
  • + 6,397 per-message parts4() queries
  • ≈ 6,525 awaited round-trips, all sequential.

Measurements (local benchmarks on the same machine / same SQLite file)

operation time
Read + JSON-parse all 24,215 part rows in one query 1.06 s
Raw per-row point query via SQLite 0.03 ms
JSON.stringify of the full assembled 34.8 MB context 0.16 s
Token estimation (length / 4) trivial

SQLite is not the bottleneck: 6,525 point queries would take ~0.2 s raw. The observed ~20 min ÷ 6,525 round-trips ≈ 180 ms per round-trip, which is Effect-runtime/IPC/layer overhead per awaited call, not storage cost. The cost scales O(session size) and is paid on every turn.

Expected behaviour

Assembling the model request for an N-message session should require a bounded number of queries (2: one for messages, one batched for parts), independent of N — e.g. SELECT ... FROM part WHERE message_id IN (batch) per page of 50, or a single JOIN. On our hardware this would bring per-turn assembly from ~20 min to well under a second.

Suggested fix

Batch parts retrieval per page inside stream9 (or join parts in the page query). Optionally raise/eliminate the fixed page size of 50 for this path, since the result is consumed wholesale by toModelMessagesEffect anyway.

Environment

  • opencode desktop 1.18.29 (also reproduced logically against CLI 1.18.11 binary, same constants)
  • Windows 11, session stored in opencode.db (SQLite, WAL)
  • Any provider (observed with an OpenAI-compatible self-hosted endpoint)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.