Describe the bug
Chat history is cached per workspace folder set, keyed by a hash of the folder paths. A linked git worktree has a different path than its main checkout, so each worktree gets its own isolated chat history and /resume answers No past chats found to resume even though the chats exist under a sibling cache dir.
The workspace set used for that key is also snapshotted at initialize and never updated, so adding the main repo to the workspace afterwards does not recover the history. That combination makes the behaviour look non-deterministic.
To Reproduce
Given ~/work/myproject (main worktree) and ~/work/myproject-worktrees/wt1 (git worktree add):
| Step |
Action |
Cache dir used |
Result |
| 1 |
Start ECA in myproject, later open a file from wt1, chat there |
myproject_<A> |
chats saved under A |
| 2 |
Another day: start ECA directly in wt1, run /resume |
wt1_<B> |
❌ "No past chats found to resume" |
| 3 |
Open a file from myproject so it joins the workspace, /resume again |
wt1_<B> |
❌ still nothing |
| 4 |
Restart ECA from myproject, then open wt1 |
myproject_<A> |
✅ chats appear |
Expected behavior
Chats belonging to a repository are resumable from any of its worktrees. Today, resumability depends on which directory the server happened to be launched from.
Additional context
Root cause is two things combining:
eca.cache/workspaces-hash hashes raw workspace paths, so hash(["…/myproject"]) ≠ hash(["…/myproject-worktrees/wt1"]) — a worktree is a separate history bucket.
:initial-workspace-folders is snapshotted in handlers/initialize and used by db/update-workspaces-cache!, while handlers/workspace-did-change-folders only updates :workspace-folders. The key is immutable for the life of the process, which is why step 3 has no effect.
(Folder order is not the problem — sorted-workspace-paths already sorts/dedupes and cache_test.clj covers it.)
On a worktree-heavy machine ~/.cache/eca/ ends up with one dir per branch rather than per repository (113 dirs here, ~10 of them worktrees of a single repo).
Proposed fix — happy to open a PR:
Key chat history by repository identity: when a workspace folder is inside a linked worktree, canonicalize it to the repo's main worktree root before hashing. Resolvable from the filesystem alone (.git file → gitdir: pointer → that dir's commondir → parent), so no git subprocess on a hot path and no PATH dependency in the native image. Memoized, since update-workspaces-cache! runs on nearly every chat mutation.
Anything that is not a linked worktree returns unchanged, so existing cache keys stay byte-identical — for a normal clone the main worktree root is the workspace path. (Keying by the shared .git dir would be a cleaner identity but would move the key for every existing user.)
Migration is nearly free: db/consolidate-workspace-cache! already merges "redundant" cache dirs newest-wins. Teaching cache/redundant-workspace-cache-files to also match the pre-canonicalization hash folds each worktree's old chats into the repo-level cache the first time it's opened.
Gated behind a config key, defaulting to on — e.g. chat.resumeAcrossWorktrees.
One caveat worth flagging: db/update-workspaces-cache! writes the whole :chats map with no cross-process lock (unlike the global auth cache, which has with-global-cache-lock). Two servers sharing a cache are last-writer-wins today already, but pooling a repo + its N worktrees into one bucket makes concurrent servers the normal case. I'd pair the fix with a read-merge-write under a per-file lock, reusing the existing merge-chats.
Questions before I write it:
- Default on, or opt-in?
- Key naming:
chat.resumeAcrossWorktrees, or something extensible like chat.historyScope: "repository" | "workspace"?
- Should MCP
authScope: "workspace" (features/tools/mcp.clj) follow the same canonicalization? Sharing tokens across worktrees seems right, but it invalidates stored tokens, so I left it out.
Describe the bug
Chat history is cached per workspace folder set, keyed by a hash of the folder paths. A linked git worktree has a different path than its main checkout, so each worktree gets its own isolated chat history and
/resumeanswersNo past chats found to resumeeven though the chats exist under a sibling cache dir.The workspace set used for that key is also snapshotted at
initializeand never updated, so adding the main repo to the workspace afterwards does not recover the history. That combination makes the behaviour look non-deterministic.To Reproduce
Given
~/work/myproject(main worktree) and~/work/myproject-worktrees/wt1(git worktree add):myproject, later open a file fromwt1, chat theremyproject_<A>wt1, run/resumewt1_<B>myprojectso it joins the workspace,/resumeagainwt1_<B>myproject, then openwt1myproject_<A>Expected behavior
Chats belonging to a repository are resumable from any of its worktrees. Today, resumability depends on which directory the server happened to be launched from.
Additional context
Root cause is two things combining:
eca.cache/workspaces-hashhashes raw workspace paths, sohash(["…/myproject"])≠hash(["…/myproject-worktrees/wt1"])— a worktree is a separate history bucket.:initial-workspace-foldersis snapshotted inhandlers/initializeand used bydb/update-workspaces-cache!, whilehandlers/workspace-did-change-foldersonly updates:workspace-folders. The key is immutable for the life of the process, which is why step 3 has no effect.(Folder order is not the problem —
sorted-workspace-pathsalready sorts/dedupes andcache_test.cljcovers it.)On a worktree-heavy machine
~/.cache/eca/ends up with one dir per branch rather than per repository (113 dirs here, ~10 of them worktrees of a single repo).Proposed fix — happy to open a PR:
Key chat history by repository identity: when a workspace folder is inside a linked worktree, canonicalize it to the repo's main worktree root before hashing. Resolvable from the filesystem alone (
.gitfile →gitdir:pointer → that dir'scommondir→ parent), so nogitsubprocess on a hot path and noPATHdependency in the native image. Memoized, sinceupdate-workspaces-cache!runs on nearly every chat mutation.Anything that is not a linked worktree returns unchanged, so existing cache keys stay byte-identical — for a normal clone the main worktree root is the workspace path. (Keying by the shared
.gitdir would be a cleaner identity but would move the key for every existing user.)Migration is nearly free:
db/consolidate-workspace-cache!already merges "redundant" cache dirs newest-wins. Teachingcache/redundant-workspace-cache-filesto also match the pre-canonicalization hash folds each worktree's old chats into the repo-level cache the first time it's opened.Gated behind a config key, defaulting to on — e.g.
chat.resumeAcrossWorktrees.One caveat worth flagging:
db/update-workspaces-cache!writes the whole:chatsmap with no cross-process lock (unlike the global auth cache, which haswith-global-cache-lock). Two servers sharing a cache are last-writer-wins today already, but pooling a repo + its N worktrees into one bucket makes concurrent servers the normal case. I'd pair the fix with a read-merge-write under a per-file lock, reusing the existingmerge-chats.Questions before I write it:
chat.resumeAcrossWorktrees, or something extensible likechat.historyScope: "repository" | "workspace"?authScope: "workspace"(features/tools/mcp.clj) follow the same canonicalization? Sharing tokens across worktrees seems right, but it invalidates stored tokens, so I left it out.