Skip to content

App crashes (OOM/StackOverflow) opening long OpenCode sessions — 91 MB response with 4,302-file summary.diffs blob #33

Description

@cloph-dsp

Summary

P4OC crashes when opening a large OpenCode session — OutOfMemoryError or StackOverflowError during JSON parsing of a single API response that can exceed 90 MB on the server's default GET /session/:id/message (no limit applied). The combination of an unbounded server response, kotlinx-serialization materialising a JsonObject tree of summary.diffs[].patch, and Compose recomputing the grouping of every message on the main thread is a guaranteed crash on any mid-range Android phone once a session accumulates enough summary.diffs blobs.

Reproduction (verified against a local OpenCode server)

  • Server: OpenCode 1.17.18, SQLite at the default user data path.
  • An affected session contains ~2,000 messages and ~8,000 parts.
  • One user message in that session is itself ~63 MB in message.data, with summary.diffs[] holding 4,302 entries totalling ~60 MB of patch strings. The largest single patch is ~1.9 MB and comes from a node_modules/ file.
  • The OpenCode server stored a unified diff of every node_modules/ file the agent touched. P4OC pulls the entire response back on every open.

Live HTTP reproducer (verified against OpenCode 1.17.18 on http://127.0.0.1:4096):

limit=null  -> 91 MB response body  (P4OC's current behaviour)
limit=100   ->  2.27 MB
limit=300   ->  4.42 MB

So the default path returns a 91 MB body. Even limit=100 is risky on heap.

Response shape

A normal assistant turn produces an info.summary like:

{
  "info": {
    "id": "msg_...",
    "role": "user",
    "summary": {
      "diffs": [
        { "file": "amui/src/foo.kt", "patch": "Index: ...\n@@ ...", "additions": 4, "deletions": 1, "status": "modified" }
      ]
    }
  },
  "parts": [...]
}

Each summary.diffs[] element has file, patch, additions, deletions, status.

Where the crash comes from (P4OC code path)

  1. ChatViewModel.kt calls sessionRepository.loadMessages(id, limit = null).
  2. SessionRepositoryImpl.kt forwards limit = null straight through Retrofit; the server returns the entire session.
  3. OpenCodeApi.getMessages(...) is @GET("session/{sessionId}/message") with @Query("limit") limit: Int?.
  4. MessageInfoDto.summary is typed JsonElement?, not MessageSummaryDto, so kotlinx-serialization stores the whole tree (thousands of JsonObject nodes per fat message) as a generic JsonElement tree. The mapper then walks the full summary.diffs[] array even though it drops the patch field afterwards.
  5. ChatScreen does messageBlocks = remember(messages) { groupMessagesIntoBlocks(messages) }, so even if parsing succeeds, recomputing block grouping on the main thread for ~2,000 messages x ~8,000 parts is very expensive.

Why it crashes (in order of likelihood, several can fire at once)

  • OutOfMemoryError during JSON parsing. A 91 MB response is buffered into one Response body -> kotlinx-serialization builds a JsonObject tree holding every patch string as its own JsonPrimitive -> 3-5x the source body in heap. Mid-range Android phones die in the JSON parser long before the value reaches P4OC code.
  • StackOverflowError on the diff tree. With summary typed as JsonElement, recursion through kotlinx-serialization's lexer blows the default stack when parsing a deeply nested array of long string elements.
  • StrictMode / ANR on the main thread. groupMessagesIntoBlocks(messages) runs on the main thread over thousands of messages with thousands of parts and 4,000+ decoders per call.

Why hiding the crash is easy

AppLog.kt only writes to android.util.Log (logcat). The release build gates d/i behind BuildConfig.DEBUG, so only w/e reach logcat on release. There is no file-based log sink. After a hard crash the logcat buffer is usually already recycled, leaving nothing for the user to attach. Add a file sink (a rotating file in getExternalFilesDir(null)/logs/...).

Suggested fix

Client (P4OC):

  • Stop defaulting loadMessages to limit = null. Default to a sane cap (e.g. 200) and paginate with the cursor the server already returns (Link: <...?before=...>; rel="next" or X-Next-Cursor: ...).
  • Don't type summary as JsonElement if you don't need to walk it. Type it as MessageSummaryDto? (already defined) and either drop patch or attach it lazily (String? on FileDiffDto) only when the user opens a diff view.
  • In ChatScreen, use messageBlocks = remember(messages, versionKey) { ... } and process the grouping in a LaunchedEffect on a default dispatcher, publishing a StateFlow<List<MessageBlock>> to the UI.
  • Add a file-based AppLog sink for debug + release so future crashes have artefacts.

Server (worth filing upstream separately): P4OC isn't the only client that will hit this. Two changes would help everyone:

  • Default GET /session/:id/message to a paginated limit (e.g. 50) when limit is omitted.
  • Strip node_modules/, .git/, dist/, build/ (or any path matching user-configured ignore) from summary.diffs[].patch before persisting, or store only file + diffstat (additions/deletions) and reconstruct the patch on demand from the actual file contents.

Workaround for now

Open the affected session from the OpenCode web UI instead of the Android client, or restore from a snapshot of opencode.db taken before the offending large message.

Diagnostics

The crash signature on device is usually an obfuscated kotlinx.serialization.json.JsonDecodingException / OutOfMemoryError / StackOverflowError chain deep inside mapMessageSummaryToDomain, often appearing as StrictMode disk-read violations during the response read. Because no file log sink exists, the practical way to debug is to add a file sink, set --log-level DEBUG on opencode serve, and watch outbound HTTP via okhttp.eventListener on a debug build.

Environment

  • OpenCode server 1.17.18 (running locally)
  • P4OC latest main (Kotlin 2.3, Compose BOM 2026.01.01, Room 2.8, OkHttp 5.3, Retrofit 3.0, kotlinx-serialization 1.10)
  • Android 16

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions