Skip to content

perf(android): long messages without freezing, and crash hardening - #84

Merged
deymosh merged 3 commits into
masterfrom
perf/android-long-messages
Oct 1, 2026
Merged

deymosh merged 3 commits into
masterfrom
perf/android-long-messages

Conversation

@deymosh

@deymosh deymosh commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

A huge markdown message froze the app or caused an ANR. The whole message was one item of the transcript list, so its markdown was parsed synchronously in composition on the main thread, and then laid out and measured in one go, again on every streaming append.

  • A long agent or user message is now cut into blocks of about 4,000 characters (markdownBlocks).
    • Cuts fall on blank lines outside code fences.
    • A fence too long for one block is closed and reopened, a long table repeats its header, and an over-long line is cut at a space.
  • The cut happens while the entry is decoded, off the main thread.
  • Each block is a list item of its own, so only the blocks on screen are composed, parsed and laid out, and streaming re-parses only the last block.
  • A plan's outline and the user's bubble are drawn across the blocks as one frame.

Crash hardening (from a scan of the app; no coroutine scope had an exception handler):

  • CoreHost.dispatch logs a refused intent (kind only, never fields) instead of crashing.
  • A transcript decode failure, a failed view read, a core that fails to open or start, a refused setOnline, a failed keep-alive, a non-derivable npub, a malformed upload URL, a non-JSON signer answer, and an out-of-bounds slash-command highlight are all contained.

Tool results were already capped at 4,000 characters by the agent host, so tool sheets stay bounded.

Test plan

  • MarkdownBlocksTest: break points, fences never left open, blank lines inside fences ignored, table header repeated, huge single line, 1 MB in well under a second.
  • testDebugUnitTest verifyPaparazziDebug: no existing golden changed; a new transcript_long snapshot was reviewed (the plan frame and bubble are continuous across cuts).
  • Device check with a very long reply (maintainer, on the prerelease).

🤖 Generated with Claude Code

deymosh and others added 3 commits October 1, 2026 03:22
A message of several hundred kilobytes froze the app or brought up an
ANR: it was one item of the transcript list, so its whole markdown was
parsed synchronously in composition on the main thread, and laid out and
measured whole, with every streaming append doing it all again.

A long agent or user message is now cut into blocks of about 4,000
characters at blank lines outside code fences (a fence too long for one
block is closed and reopened, a long table repeats its header, an
over-long line is cut at a space). The cut happens as the entry is
decoded, off the main thread, and each block is a list item of its own.
Only the blocks on screen are composed, parsed and laid out, and a
streaming message re-parses only its last block. A plan's outline and the
user's bubble are drawn across the blocks as one frame, bridging the
list's spacing.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Screens hand intents to the core from fire-and-forget coroutines, and
the core's dispatch throws for a value it does not know (and a fault in
the core surfaces as an exception too), which crashed the app. Dispatch
now logs the intent's kind and the error, never its fields (they may hold
secrets), and goes on; cancellation still propagates.

Co-Authored-By: Claude Code <noreply@anthropic.com>
No coroutine scope in the app has an exception handler, so a throw in
any of these paths took the process down. Each now logs (the exception's
class and a short message, never payloads) and carries on:

- a transcript row the decoders refuse ends that session's updates
  instead of crashing;
- a failed read of one of the core's views keeps the refresher alive;
- the connection service survives a core that fails to open or start
  (it stops itself, as it does with no login), a refused setOnline, and
  a failed keep-alive probe;
- the Settings hub shows no npub rather than crashing if it cannot be
  derived;
- a malformed upload URL and a signer answer that is not JSON become the
  failures their callers already handle;
- the slash-command highlight no longer reads past a name whose
  lowercase form is longer than the name.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@deymosh
deymosh merged commit fec8723 into master Oct 1, 2026
6 checks passed
@deymosh deymosh mentioned this pull request Oct 1, 2026
@deymosh
deymosh deleted the perf/android-long-messages branch October 2, 2026 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant