[Bug]: Desktop typing freezes 1.5–2 s every ~30 s while storage.saveThread re-encodes a large thread snapshot on the main thread#29417

opened7d agobyWWhiteWarrior625

What happens

On the Linux desktop app, typing stops appearing for 1.5–2 seconds about every 20–30 seconds. The window's renderer main thread is busy for that whole time. The freezes continue while the user is viewing a different, small thread.

Version: 0.1.7-nightly.20261102.4080 (AppImage, Linux, Wayland). The same memory churn in the renderer was present on the previous nightly.

Cause

Each freeze is one storage.saveThread call for a large remote thread that the renderer still holds. The user was not viewing that thread. Its encoded cache value is about 47 MB. saveThread encodes the whole OrchestrationV2ThreadDetailSnapshot with Effect Schema into one JSON string and writes it to IndexedDB. The work runs on the main thread in one step and never yields.

Upstream source (main at ac8dce0c):

  • packages/client-runtime/src/state/threads.ts: EnvironmentThreadState.persist, queued from queuePersist(...) after a run settles and again in the settled-items pass (settleTurnItems → queuePersist({ ...saved, projection })).
  • apps/web/src/connection/storage.ts: storage.saveThread → encodeStoredThreadSnapshot(...) → writeDatabaseValue(...).

The comment above the first queuePersist says saves wait until the run settles "so cache encoding stays off the streaming path". A thread that runs many short agent turns settles every 30–50 s. So in practice it re-encodes the full snapshot each time, and each settle produces two saves about 11.6 s apart.

Evidence

  • Renderer CPU profile (CDP Profiler on the main webContents, 60 s, 0.5 ms sampling). There were two busy runs of 1.6 s, at 10:35:46.7 and 10:35:58.4. Both have the same profile:
    • ~97% of samples sit under one Effect fiber.
    • Self time: 41% JSON.stringify (the Schema fromJsonString encode try), 20% Schema struct onItem, 15% native IDBObjectStore.put, 7% Schema encode.
    • Only ~2% is GC.
  • Client spans forwarded to the server trace: storage.saveThread / EnvironmentThreadState.persist started at 10:35:46.717 (1648 ms) and 10:35:58.381 (1702 ms). Each matches a profiled busy run within 30 ms.
  • Over ~20 minutes, 34 saveThread spans took 1.5–2.7 s each. They came in pairs about 11.6 s apart, with a new pair every 30–50 s. Every typing freeze we timed (via /proc per-thread CPU at 250 ms) falls on one of them.
  • IndexedDB: a 47 MB value appeared in t3code_app_0.indexeddb.blob one second after each of those saves. Its header shows schemaVersion: 5, a remote environmentId, and a snapshotSequence that rose by 1 between the two saves of a pair. That thread has ~1,860 snapshot sequences and a long tool history.
  • The renderer working set swings by 300–400 MB per minute (e.g. 572 → 931 MB between two 1-minute renderer-history.ndjson samples). Peak is 1.6 GB.

Suggested fixes

Any one of these would remove the freeze:

  1. Skip or cap the thread cache for very large snapshots. For example, persist only the bounded tail window that a cold start needs, not the full loaded history.
  2. Do the encode and write off the main thread (a worker or structured clone of a plain object), or in chunks that yield to input.
  3. Throttle persist per thread: at most one save per N minutes while the thread keeps settling, plus a final save on unmount.
  4. Persist only threads the user is viewing. A thread kept alive by the resume cache doesn't need a save after every settle.

Related, not duplicates: #9661 (W14: IndexedDB caches have no size or age eviction) and #22985 (renderer freeze from reasoning previews).