Desktop app: latest assistant messages stop rendering while the transcript on disk stays complete and intact
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
In the macOS desktop app, the most recent assistant messages frequently stop appearing in the chat view. The conversation looks like it ends at an older message, with a loading indicator left underneath it, even though the response has already completed.
This is not data loss. I checked the on-disk transcript (~/.claude/projects/<project>/<session-id>.jsonl) while the bug was visible on screen: the "missing" messages were present, complete, and structurally intact. The storage layer is fine — the renderer never paints the tail.
It happens during a live session, not only after a restart, and restarting does not recover the view. Quitting cleanly with Cmd+Q rather than force-quitting makes no difference, which is consistent with the write path never having been at fault.
The session where I confirmed this was only 48 KB / 133 records, so transcript size is not the cause. I had initially suspected size, since other sessions of mine had grown to 300 MB — trimming one of them to 29 MB changed nothing.
Because the app looks stuck, the natural reaction is to force-quit, which is how this gets mistaken for data loss.
What Should Happen?
The chat view should show the conversation through the last message and be scrolled to the bottom — both while the session is open and after restarting the app. Once a response is complete on disk, it should be committed to the view and the loading indicator should clear.
Error Messages/Logs
No error is surfaced anywhere — that is part of the problem. The app shows no failure, just a stalled view with a loading indicator under a response that is already complete on disk.
Integrity checks I ran against the affected session's `.jsonl` while the bug was visible on screen (48 KB, 133 records):
| Check | Result |
|---|---|
| Malformed JSON lines | 0 |
| Records with a `parentUuid` not present in the file | 0 |
| Sibling branches (two records sharing one parent) | none |
| The two messages missing from the view | both present and complete, correct timestamps |
| Record types present | `user`, `assistant`, `attachment`, `last-prompt`, `ai-title`, `custom-title`, `mode`, `queue-operation`, `system` |
The message that never appeared on screen was fully serialized to disk about one second after it was generated. The chain is unbroken and single-threaded, so there is no alternate branch the UI could be following instead.
I also checked whether this could be write truncation from force-quitting. It is not: across a dozen sessions there were no malformed lines anywhere, and every "user turn with no assistant reply" turned out to be an explicit `[Request interrupted by user]` record rather than a truncated write.
Steps to Reproduce
I have not found a deterministic trigger, but across a dozen sessions it correlates strongly with the conditions below.
- Open a session in the macOS desktop app.
- Send a request that produces several consecutive tool calls in one turn. Long-running tools make it much more likely (browser automation, video rendering, large file processing).
- While the response is streaming, switch away from the window — or cancel a tool-permission prompt to interrupt the turn.
- Observe that the chat view stops updating from some point onward, leaving a loading indicator under the last rendered message.
- Send another message. It is accepted and answered, and the answer is written to
~/.claude/projects/<project>/<session-id>.jsonl, but the view stays frozen at the older message. - Quit the app with Cmd+Q and reopen the session. The view still does not show the latest messages, while the transcript file contains all of them.
To confirm step 5/6 independently of the UI, read the last records of the session file:
python3 -c "
import json,sys
for line in open(sys.argv[1]):
d=json.loads(line)
if d.get('type') in ('user','assistant') and d.get('timestamp'):
print(d['timestamp'], d['type'])
" ~/.claude/projects/<project>/<session-id>.jsonl | tail -20
The timestamps continue past the last message visible in the app.
Claude Model
None
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.229 — macOS desktop app. This is what the app's own updater installs as of 2026-08-15 (I updated and it auto-restarted, and the version stayed at 2.1.229). Taken from the version field in session transcript records, since claude --version is not on PATH for the desktop install. For reference, the npm registry shows 2.1.233 as latest for @anthropic-ai/claude-code, so the desktop app appears to ship on a separate track.
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Hypothesis
The renderer appears to stay in a streaming state that never resolves — the completion event that should commit the message to the view seems to be lost or never delivered. The leftover loading indicator under a response that is already finished on disk points that way.
Ruled out
- Transcript size — reproduces in a 48 KB / 133-record session.
- Write truncation from force-quitting — no malformed lines in any session checked; every apparently unanswered user turn was an explicit
[Request interrupted by user]record. - Broken parent chain / branching —
parentUuidchain is complete and single-threaded in the affected session. - Being out of date — I ran the app's updater before filing. It auto-restarted and the version stayed at 2.1.229, and the bug reproduced again afterwards.
Related issues (similar area, different symptom)
- #84858 — desktop app shows "no messages yet" after restart, transcript intact. That one goes fully blank and only after a restart; mine renders most of the history but stalls at the tail, and it happens during a live session too. Likely the same subsystem (transcript hydration / render commit), different failure mode.
- #86689 — mid-turn messages not rendered, labeled
area:tui. Mine is the desktop app, and it is the assistant's completed responses that fail to paint, not the user's messages.
Most recent occurrence
It recurred immediately after a turn that was interrupted by cancelling a tool-permission prompt: the interrupt was recorded, my next message was answered and written to disk, and the view never showed either. That matches step 3 in the reproduction steps above.
Environment note
This is the macOS desktop app, not a terminal. The "Terminal/Shell" field has no matching option, so I left the default; please read it as "desktop app, no terminal involved".
Workaround
None found in the UI. The content can be read out of ~/.claude/projects/<project>/<session-id>.jsonl.
3 Comments
Possibly the same root cause, with a concrete mechanism we were able to measure on macOS: the same project gets two session stores under
~/.claude/projects/, keyed by different Unicode normalizations of the project path. The transcript on disk stays complete because it is split across two directories, and a restart can render the frozen one.Measurement
On macOS, folder names are stored per-segment with mixed normalization. In our case the path has:
| Folder on disk | Normalization | Codepoints |
|---|---|---|
|
Dropbox/프로젝트| NFD | 9 ||
프로젝트/C9-문항별 스캐폴딩 힌트 제작| NFC | 17 |When Claude Code slugifies that path into a project key (non-ASCII →
-), the result depends on whether the rawreaddirvalue or an NFC-normalized value is used:Both directories exist, and the same session IDs live in both.
Observed write pattern
The two files are in a strict byte-prefix relation. The variant copy's last user message is the message immediately before the restart; the canonical copy has everything after.
The pattern is:
So the apparent "rollback window" is not session length or a compaction boundary — it equals the interval since the previous restart. In our case that read as "the conversation jumped back 19–20 hours", which is why we initially misdiagnosed it as a long-session rendering issue.
Scope on one machine
Reproduction sketch
~/.claude/projects/— two directories differing only in hyphen-run length, both containing the same session ID..jsonl.To check the per-segment normalization:
Local mitigation we are using
A
SessionStarthook that trashes a variant store only when every gate passes: the pair must share at least one session ID; the stale copy must be a byte-prefix of the live one across all shared sessions (mtime is unreliable here — right after a restart the variant is the newer file); every entry in the stale directory, including subdirectories and non-.jsonlfiles, must be present and identical in the live one; the stale copy must have been untouched for 10 minutes; and no open file handles. Anything else is held for a human.This only stops accumulation. It cannot prevent the first stale render after a restart, and it does not stop the app from recreating the variant directory — that needs a fix in the path normalization used to derive the project key.
Happy to provide raw directory listings or normalization dumps if useful.
Additional evidence from
~/Library/Logs/Claude/main.log, which narrows this to the view layer rather than transcript loading.1. Loading is fine — the failure is downstream
The main log records a load count per session:
For the affected session at that exact moment, the transcript file contained 642 raw records / 96 messages with text / 52 conversational turns. So "48 transcript messages" is a turn-level count and essentially matches the file — the app read the transcript correctly and then did not paint the result. I initially mistook 48-vs-431-records for a truncated load; it is not. Loading is healthy.
That means the defect sits after the read: the messages are in memory and never reach the view.
2. Clearing Chromium caches does not help
Ruled out by deleting
Cache(1.0 GB),Code Cache(276 MB),GPUCache,DawnWebGPUCache,DawnGraphiteCacheunder~/Library/Application Support/Claude/, then relaunching. No change — the tail still fails to render.3. Separate finding: transcripts over 50 MB are silently truncated
Not the cause of this bug (it drops the oldest content while the symptom is newest content missing), but worth its own look:
Six of my sessions cross that threshold (291 MB, 208 MB, 149 MB, 110 MB, 90 MB, 84 MB). For those, earlier history is not loaded at all and the UI gives no indication — the conversation simply appears to begin partway through. If that cap is intentional, surfacing it in the UI ("earlier messages not loaded") would save people from assuming data loss.
4. Version reporting is inconsistent
The app's bundled binary is at
~/Library/Application Support/Claude/claude-code/2.1.234/, while theversionfield written into session transcript records says2.1.229. I reported 2.1.229 above because that is what the transcripts record; the actual binary in use is 2.1.234. Worth aligning, since transcriptversionis the field a user is most likely to quote in a bug report.Summary
Transcript write path: healthy. Transcript read path: healthy. Caches: not involved. The gap is between "messages loaded into memory" and "messages rendered", and it persists across relaunches.
@kangj21's diagnosis is correct, and this is my root cause too. Confirming independently on a second machine — different sync client, different language segments, same mechanism. My earlier comment about "loading is healthy" was wrong: the load was complete, but of the stale store.
Measurement
Path:
/Users/<me>/Library/CloudStorage/OneDrive-<KO>/<KO>/<KO>— created by the OneDrive client, which stores the segments as NFD.Two project keys exist, differing only in hyphen-run length:
18 session IDs exist in both, and in every case the variant is a strict byte-prefix of the live copy:
| session | live | variant (frozen) |
|---|---|---|
| a | 08-19 · 137 MB | 08-11 · 90 MB |
| b | 08-19 · 323 MB | 08-12 · 291 MB |
| c | 08-19 · 116 MB | 08-12 · 84 MB |
| d | 08-19 · 156 MB | 08-12 · 0.3 MB |
The app reads the variant
This is the part that ties it to the rendering symptom.
main.log:290908586 B = 290.9 MB, which is the variant's size. The live copy is 323 MB. So the loader resolved the frozen key. Likewise
Loaded 54 transcript messagesfor the session whose variant is 0.3 MB while the live copy is 156 MB.That also explains the "how far back it jumps" pattern: the freeze points cluster at 08-11/08-12, which were restart moments, exactly as described above.
What the user actually sees
The rendered conversation stops at the previous restart. It is not a streaming/commit failure as I originally guessed — the view is faithfully rendering a file that stopped being written days ago.
Mitigation in use here
Copying live → variant (only when the variant is verified as a strict byte-prefix, never writing the live copy, never deleting) makes the next launch render current history. 471 MB of conversation reappeared. It has to be repeated, since the variant re-freezes at each restart, so it runs on a 5-minute timer.
Renaming the path segments to NFC would collapse the two keys, but the NFD segment here is the OneDrive sync root — renaming it risks the sync link, and the client would likely recreate it as NFD. Not a viable user-side fix.
The real fix is normalizing the path consistently before deriving the project key. Until then, a one-line guard would help a lot: if a resolved project key has a sibling key that differs only by hyphen-run length and shares session IDs, prefer the one with the newer last row — or at minimum surface it, instead of silently rendering a days-old transcript.
Happy to supply directory listings or per-segment normalization dumps.