Skip to content

Commit 0af24fb

Browse files
committed
Store chats in per-chat cache files instead of one workspace blob
Every history append serialized the entire workspace chat cache (hundreds of MiB for heavy users), causing CPU spikes between messages and slow startup. Chats now live in chats/<chat-id>.transit.json plus a small index that drives listings and lazy loading, so saving a message costs O(that chat) and startup only reads the index. - migrate the legacy db.transit.json on load, keeping it as .bak so rolling back to an older ECA loses nothing - keep the global auth cache at version 6 so downgrades stay logged in - index merge-on-write keeps the worktree sharing semantics from #558, chat files are recency guarded against stale peer overwrites - hooks db_cache_path now points at the workspace cache dir and read-chat accepts both the new layout and legacy files - isolate unit tests cache dir, they were polluting the real user cache for the eca workspace Closes #557
1 parent cf1b653 commit 0af24fb

19 files changed

Lines changed: 1211 additions & 403 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Store chats in per-chat cache files with a lazy-loaded index instead of one whole-workspace blob, fixing CPU spikes and slow startup as history grows; legacy caches migrate automatically. #557
6+
- Hooks `db_cache_path` now points at the workspace cache dir instead of `db.transit.json`; `read-chat --db-cache-path` accepts the dir or a legacy file. #557
57
- Keep `compact_chat` in the tool schema across normal and compact requests, preserving prompt-cache prefixes while rejecting calls outside active compaction.
68

79
- Provide token metrics for ollama provider. #567.

docs/config/hooks.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ Every hook receives these fields on stdin:
166166

167167
- `hook_name`, `hook_type`, `workspaces`, `cwd`, `db_cache_path`, `session_id`, `eca_executable`
168168
- `cwd` — the first workspace folder, matching the working directory ECA uses for hook commands.
169-
- `session_id` — the cache session key derived from the `db_cache_path` parent directory.
169+
- `db_cache_path` — the workspace chat cache dir (holding `chats/index.transit.json` and per-chat files); pass it to `eca read-chat --db-cache-path`.
170+
- `session_id` — the cache session key: the `db_cache_path` dir name.
170171
- `eca_executable` — the launch command of the running ECA process (executable path for native binaries; the full `java ... -jar` invocation for JVM launches).
171172

172173
**Chat-scoped hooks** (everything except `sessionStart`/`sessionEnd`) additionally receive `chat_id`, `agent`, `behavior` (deprecated alias of `agent`), `full_model`, and `variant`.
@@ -377,7 +378,7 @@ eca_executable=$(echo "$input" | jq -r '.eca_executable')
377378
$eca_executable read-chat --db-cache-path "$db_cache_path" --chat-id "$chat_id"
378379
```
379380
380-
Keep the query focused and fast — the hook runs synchronously with a 30s timeout. `read-chat` needs no running server and reads directly from `db.transit.json`. See [`read-chat`](../read-chat.md) for full options and scripting examples.
381+
Keep the query focused and fast — the hook runs synchronously with a 30s timeout. `read-chat` needs no running server and reads directly from the chat cache files. See [`read-chat`](../read-chat.md) for full options and scripting examples.
381382
382383
## `/hooks` command
383384

docs/read-chat.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,26 @@ description: "Stream raw ECA chat history from the DB cache as JSONL: list chats
44

55
# read-chat
66

7-
Inspect ECA's chat database offline by reading `db.transit.json` directly. No running server is required.
7+
Inspect ECA's chat database offline by reading the workspace chat cache directly. No running server is required.
8+
9+
`--db-cache-path` takes the workspace cache dir (e.g. `~/.cache/eca/myproj_a1b2C3d4`, holding `chats/index.transit.json` plus one `chats/<chat-id>.transit.json` per chat) and also accepts a legacy `db.transit.json` file written by older ECA versions.
810

911
Output is always **raw JSONL** (one JSON object per line) in the persisted internal shape, making it suitable for debugging, export, and programmatic processing.
1012

1113
## Quick start
1214

1315
```bash
1416
# List all chats (newest first)
15-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json
17+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4
1618

1719
# Resolve the cache from the session's workspace folders
1820
eca read-chat --workspace ~/Code/Clojure/eca --workspace ~/Code/Clojure/eca-worktrees/add-read-chat-command
1921

2022
# Stream raw messages from a specific chat
21-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json --chat-id <chat-id>
23+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 --chat-id <chat-id>
2224

2325
# Last 10 user messages from the past hour
24-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
26+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
2527
--chat-id <chat-id> --since 1h --role user | tail -n 10
2628
```
2729

@@ -47,7 +49,7 @@ Provide exactly one input source: `--db-cache-path` or one or more `--workspace`
4749

4850
| Option | Description |
4951
|--------|-------------|
50-
| `--db-cache-path <PATH>` | Path to `db.transit.json`. |
52+
| `--db-cache-path <PATH>` | Workspace cache dir (or a legacy `db.transit.json` file). |
5153
| `--workspace <PATH>` | Workspace folder; repeat in the original session order. |
5254
| `--chat-id <ID>` | Focus on a chat; omit to list all chats. |
5355
| `--role <ROLE>` | Filter messages by exact `role` string (requires `--chat-id`). |
@@ -85,21 +87,21 @@ Messages created before `:created-at` tracking was added lack timestamps. When f
8587
=== "Last 10 messages"
8688

8789
```bash
88-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
90+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
8991
--chat-id abc | tail -n 10
9092
```
9193

9294
=== "Count messages"
9395

9496
```bash
95-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
97+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
9698
--chat-id abc | wc -l
9799
```
98100

99101
=== "Extract user text"
100102

101103
```bash
102-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
104+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
103105
--chat-id abc --role user | \
104106
jq -r '
105107
def content_text:
@@ -120,23 +122,23 @@ Messages created before `:created-at` tracking was added lack timestamps. When f
120122
=== "Extract assistant text"
121123

122124
```bash
123-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
125+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
124126
--chat-id abc --role assistant | \
125127
jq -r '.content[]? | select(.type == "text") | .text'
126128
```
127129

128130
=== "List tool calls"
129131

130132
```bash
131-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
133+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
132134
--chat-id abc --role tool_call | \
133135
jq -c '.content | {id, name, input: .arguments, summary, server, full_name: ."full-name"}'
134136
```
135137

136138
=== "List tool outputs"
137139

138140
```bash
139-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
141+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
140142
--chat-id abc --role tool_call_output | \
141143
jq -c '
142144
.content
@@ -153,28 +155,28 @@ Messages created before `:created-at` tracking was added lack timestamps. When f
153155
=== "Find chats by model"
154156

155157
```bash
156-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json | \
158+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 | \
157159
jq -c 'select(.model | contains("claude"))'
158160
```
159161

160162
=== "Count user prompts"
161163

162164
```bash
163-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json | \
165+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 | \
164166
jq '{id: .id, count: .["user-prompt-count"]}'
165167
```
166168

167169
=== "Export a chat"
168170

169171
```bash
170-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json \
172+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 \
171173
--chat-id abc > chat-export.jsonl
172174
```
173175

174176
=== "Recent chats, pretty-printed"
175177

176178
```bash
177-
eca read-chat --db-cache-path ~/.cache/eca/db.transit.json --since 1d | \
179+
eca read-chat --db-cache-path ~/.cache/eca/myproj_a1b2C3d4 --since 1d | \
178180
jq '{title, model, updated_at: .["updated-at"]}'
179181
```
180182

src/eca/cache.clj

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575

7676
(def ^:private linked-worktree-root
7777
"Memoized `linked-worktree-root*`: resolved on nearly every chat mutation via
78-
`eca.db/update-workspaces-cache!`, and a worktree's gitdir pointer does not
78+
`eca.db/save-chat!`, and a worktree's gitdir pointer does not
7979
change during a server's lifetime."
8080
(memoize linked-worktree-root*))
8181

@@ -142,13 +142,19 @@
142142
(str sanitized "_" hash)
143143
hash)))
144144

145-
(defn workspace-cache-file
146-
"Returns a File object for a workspace-specific cache file.
145+
(defn workspace-cache-dir
146+
"Returns a File object for the workspace-specific cache directory.
147147
The directory identity is the order-independent <hash>; the human-readable
148148
prefix is cosmetic. Healing of caches fragmented across differently-named
149-
dirs for the same workspace is handled by eca.db/consolidate-workspace-cache!."
149+
dirs for the same workspace is handled by eca.db/migrate-legacy-workspace-caches!."
150+
^File [workspaces uri->filename-fn]
151+
(io/file (global-dir) (workspace-dir-name workspaces uri->filename-fn)))
152+
153+
(defn workspace-cache-file
154+
"Returns a File object for a workspace-specific cache file inside
155+
`workspace-cache-dir`."
150156
[workspaces filename uri->filename-fn]
151-
(io/file (global-dir) (workspace-dir-name workspaces uri->filename-fn) filename))
157+
(io/file (workspace-cache-dir workspaces uri->filename-fn) filename))
152158

153159
(defn redundant-workspace-cache-files
154160
"Returns the cache files named `filename` that live in directories belonging to

0 commit comments

Comments
 (0)