Desktop app: latest assistant messages stop rendering while the transcript on disk stays complete and intact

Status Fixed / completed
Reported on v2.1.229
Maintainer reply None cached
Activity 3 comments · opened Aug 15, 2026 · closed Aug 25, 2026

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.

  1. Open a session in the macOS desktop app.
  2. 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).
  3. While the response is streaming, switch away from the window — or cancel a tool-permission prompt to interrupt the turn.
  4. Observe that the chat view stops updating from some point onward, leaving a loading indicator under the last rendered message.
  5. 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.
  6. 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 / branchingparentUuid chain 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.

View original on GitHub ↗

3 Comments

kangj21 · 15 days ago

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 raw readdir value or an NFC-normalized value is used:

-Users-jhk-Library-CloudStorage-Dropbox------C9---------------       (6 hyphens, NFC segment)
-Users-jhk-Library-CloudStorage-Dropbox-----------C9---------------  (11 hyphens, raw/NFD segment)

Both directories exist, and the same session IDs live in both.

Observed write pattern

canonical key : 2afbcf3f-….jsonl  3,076,221 B  last row 17:09
variant key   : 2afbcf3f-….jsonl  3,054,438 B  last row 17:07  ← frozen at restart time

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:

  1. At restart, a full snapshot is written to the variant key (a whole-file write, not an append).
  2. Afterwards, appends go only to the canonical key. The variant copy is frozen at that instant.
  3. If the next restart resolves the variant key, the UI renders the transcript as of the previous restart.

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

  • 55 project directories total, 6 duplicate groups, 3 of them holding real data.
  • Only projects with non-ASCII (Korean) path segments are affected.
  • Deleting the variant directories is not sufficient — they were recreated within minutes of the next restart.
  • No data was ever lost. This is a display/selection problem, not a storage problem. (Worth noting: because the stores are separate, the data stays safe. Symlinking the two keys together would be actively dangerous, since the restart-time whole-file write could clobber the canonical transcript.)

Reproduction sketch

  1. Put a project under a path with a non-ASCII segment stored as NFD on disk (common for folders created by or synced through some clients on macOS).
  2. Open a session, exchange a few messages, restart the app.
  3. Inspect ~/.claude/projects/ — two directories differing only in hyphen-run length, both containing the same session ID.
  4. Restart again and compare the rendered transcript against the larger .jsonl.

To check the per-segment normalization:

python3 -c "import unicodedata,os,sys; [print(len(n), n==unicodedata.normalize('NFC',n), n) for n in os.listdir(sys.argv[1])]" '<parent dir>'

Local mitigation we are using

A SessionStart hook 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-.jsonl files, 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.

mong7298-alt · 11 days ago

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:

[info] Loaded 48 transcript messages for session local_23463d22-...

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, DawnGraphiteCache under ~/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:

[warn] [CCD] Session <id> transcript is 290908586 bytes; tail-loading last 50 MB

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 the version field written into session transcript records says 2.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 transcript version is 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.

mong7298-alt · 11 days ago

@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:

…-CloudStorage-OneDrive-------------              60 sessions, 2357 MB   ← live (appends land here)
…-CloudStorage-OneDrive-------------------------  18 sessions, 1028 MB   ← variant (frozen)

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:

[warn] [CCD] Session <id> transcript is 290908586 bytes; tail-loading last 50 MB

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 messages for 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.