ECA persists all chats for a workspace in one db.transit.json. Most persisted history additions invoke update-workspaces-cache!, which normalizes the complete :chats map, Transit-encodes it into a temporary file, and renames that file over the previous cache.
As workspace history grows, the cost of adding one assistant message, reasoning result, or tool result therefore scales with all retained workspace history, including unrelated chats and completed subagent chats.
This appears to cause:
- brief 100% CPU spikes at chat-step boundaries;
- substantial spikes after tool results;
- increasing latency as a workspace accumulates history;
- repeated serialization and disk output during tool-heavy turns;
- expensive startup deserialization.
Relevant code
The durable save cadence was introduced in “Persist chats more durably so /resume keeps long and errored chats” on May 22, 2026. The durability goal seems valid; this issue is about the resulting performance characteristics once the cache becomes large.
To Reproduce
- Use ECA normally in one workspace with persistent chat caching enabled.
- Accumulate many chats or subagent runs, especially tool-heavy runs.
- Observe the size of the workspace’s
db.transit.json.
- Start a prompt that performs one or more tool calls.
- Observe CPU usage and the cache file’s modification time while tool results are returned.
- Restart ECA and observe initialization while the complete cache is decoded.
In the workspace where I observed the problem, a sanitized snapshot contained:
| Measurement |
Value |
| Cache size |
315,964,557 bytes / 301.33 MiB |
| Chats |
726 |
| Subagent chats |
700 |
| Messages |
76,842 |
| Tool calls |
28,066 |
| Tool outputs |
28,066 |
| Reasoning messages |
18,615 |
No chat IDs, workspace IDs, titles, prompts, paths, tool arguments, tool outputs, or other message content are included here.
Repeated full-cache measurements on this machine produced:
| Operation |
Observed range |
| Transit decode |
1.17–1.42 seconds |
| Transit encode to a null output stream |
0.98–1.15 seconds |
| Maximum benchmark-process RSS |
1.73 GiB |
The encode measurement excludes writing the approximately 301 MiB temporary file and renaming it.
At the measured cache size:
| Physical save requests |
Logical payload serialized/written |
| 1 |
approximately 301 MiB |
| 10 |
approximately 2.94 GiB |
| 100 |
approximately 29.43 GiB |
These are application-level payload estimates. Physical device writes will vary with filesystem caching and other operating-system behavior.
A typical tool-heavy turn can perform:
- A save for assistant text before the tool batch, if present.
- One save for each completed or rejected tool result.
- A save for final assistant text.
- Another save from prompt completion.
This is not a save per streamed token. The issue occurs when completed history records are appended and at lifecycle persistence boundaries.
Expected behavior
The cost of persisting one chat-history mutation should remain reasonably bounded as unrelated chats and completed subagent histories accumulate.
Durable mid-turn recovery should not require repeatedly serializing and replacing hundreds of megabytes of unrelated history for each tool result or lifecycle transition.
Additional context
Impact
The observed behavior has several related effects:
- A single small history mutation can trigger serialization of the complete workspace history.
- Parallel tool results can queue several complete saves.
- Completed subagent histories contribute to every later save, even when unrelated to the active chat.
- Normal completion may save both when the assistant message is appended and again during prompt finalization.
- Defensive error saves can repeat the same expensive operation during one logical failure transition.
- Startup eagerly reads and materializes the complete cache.
The temporary-file-and-rename implementation protects against partially written destination files, but it does not reduce the amount of data serialized.
Cache composition
A content-size proxy for the sanitized snapshot indicated that the largest contributors were:
| Role |
Approximate content-size proxy |
tool_call_output |
185.97 MiB |
tool_call |
45.79 MiB |
reason |
39.68 MiB |
assistant |
5.74 MiB |
user |
2.11 MiB |
These are UTF-8 byte counts of printed message content, not exact Transit-file byte attribution. They are included only to show that tool-heavy and subagent-heavy histories can grow the workspace cache quickly.
Retention
Startup cleanup currently uses a default retention period of 14 days:
The cleanup comparison uses :created-at, rather than recent activity through :updated-at. Cleanup also performs another complete cache rewrite when chats are removed.
This may be intentional and is secondary to the write-amplification issue, but it may affect how quickly active workspaces accumulate large caches.
Possible directions for discussion
I do not have a specific fix to propose because the right approach depends on ECA’s desired durability and history-retention semantics.
Possible design areas include:
- reducing redundant saves at lifecycle boundaries;
- coalescing overlapping save requests;
- persisting only the changed chat;
- using incremental or append-oriented persistence;
- separating chat metadata from message history;
- changing completed-subagent retention;
- limiting or externalizing persisted tool-output history;
- warning or exposing metrics when a workspace cache becomes unusually large.
Coalescing alone would reduce repeated writes in a burst, but one save would still serialize the entire cache. More incremental storage would be a larger architectural change.
Secondary CPU observations
While investigating, I found two additional whole-history/request processing paths. These are separate from cache-write amplification and may deserve separate issues.
Request logging formats the request before checking whether debug logging is enabled:
This can stringify a complete provider request even when the resulting debug message is discarded.
Context-usage breakdown traverses and stringifies the complete message history:
These may contribute to smaller request/usage-boundary CPU spikes, but they do not explain the repeated 301 MiB cache writes.
Open Questions
- Is a durable complete-cache save intended after every persisted history append, including every subagent tool result?
- Is a workspace cache of hundreds of megabytes considered within the expected operating range?
- How much history is expected to survive a crash during a long tool or subagent loop?
- Is detailed completed-subagent history intended to remain indefinitely available until generic chat retention removes it?
- Is retention intentionally based on
:created-at rather than :updated-at?
- Would it be useful for
:db/upsert-cache metrics to expose serialized bytes and save duration, or warn above a size/latency threshold?
- Should the request-log formatting and context-breakdown observations be filed separately?
ECA persists all chats for a workspace in one
db.transit.json. Most persisted history additions invokeupdate-workspaces-cache!, which normalizes the complete:chatsmap, Transit-encodes it into a temporary file, and renames that file over the previous cache.As workspace history grows, the cost of adding one assistant message, reasoning result, or tool result therefore scales with all retained workspace history, including unrelated chats and completed subagent chats.
This appears to cause:
Relevant code
add-to-history!persists most appended history recordsupdate-workspaces-cache!selects and normalizes the complete:chatsmapupsert-cache!serializes the complete value to a temporary file and replaces the destinationtool_call_output, triggering a savefinish-chat-prompt!performs another workspace-cache saveThe durable save cadence was introduced in “Persist chats more durably so /resume keeps long and errored chats” on May 22, 2026. The durability goal seems valid; this issue is about the resulting performance characteristics once the cache becomes large.
To Reproduce
db.transit.json.In the workspace where I observed the problem, a sanitized snapshot contained:
No chat IDs, workspace IDs, titles, prompts, paths, tool arguments, tool outputs, or other message content are included here.
Repeated full-cache measurements on this machine produced:
The encode measurement excludes writing the approximately 301 MiB temporary file and renaming it.
At the measured cache size:
These are application-level payload estimates. Physical device writes will vary with filesystem caching and other operating-system behavior.
A typical tool-heavy turn can perform:
This is not a save per streamed token. The issue occurs when completed history records are appended and at lifecycle persistence boundaries.
Expected behavior
The cost of persisting one chat-history mutation should remain reasonably bounded as unrelated chats and completed subagent histories accumulate.
Durable mid-turn recovery should not require repeatedly serializing and replacing hundreds of megabytes of unrelated history for each tool result or lifecycle transition.
Additional context
Impact
The observed behavior has several related effects:
The temporary-file-and-rename implementation protects against partially written destination files, but it does not reduce the amount of data serialized.
Cache composition
A content-size proxy for the sanitized snapshot indicated that the largest contributors were:
tool_call_outputtool_callreasonassistantuserThese are UTF-8 byte counts of printed message content, not exact Transit-file byte attribution. They are included only to show that tool-heavy and subagent-heavy histories can grow the workspace cache quickly.
Retention
Startup cleanup currently uses a default retention period of 14 days:
cleanup-old-chats!The cleanup comparison uses
:created-at, rather than recent activity through:updated-at. Cleanup also performs another complete cache rewrite when chats are removed.This may be intentional and is secondary to the write-amplification issue, but it may affect how quickly active workspaces accumulate large caches.
Possible directions for discussion
I do not have a specific fix to propose because the right approach depends on ECA’s desired durability and history-retention semantics.
Possible design areas include:
Coalescing alone would reduce repeated writes in a burst, but one save would still serialize the entire cache. More incremental storage would be a larger architectural change.
Secondary CPU observations
While investigating, I found two additional whole-history/request processing paths. These are separate from cache-write amplification and may deserve separate issues.
Request logging formats the request before checking whether debug logging is enabled:
llm-util/log-requestlogger/debugchecks the level after its arguments have been evaluatedThis can stringify a complete provider request even when the resulting debug message is discarded.
Context-usage breakdown traverses and stringifies the complete message history:
context-breakdownThese may contribute to smaller request/usage-boundary CPU spikes, but they do not explain the repeated 301 MiB cache writes.
Open Questions
:created-atrather than:updated-at?:db/upsert-cachemetrics to expose serialized bytes and save duration, or warn above a size/latency threshold?