Skip to content

/resume finds no past chats when ECA is started from a git worktree #558

Description

@ovistoica

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:

  1. eca.cache/workspaces-hash hashes raw workspace paths, so hash(["…/myproject"])hash(["…/myproject-worktrees/wt1"]) — a worktree is a separate history bucket.
  2. :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:

  1. Default on, or opt-in?
  2. Key naming: chat.resumeAcrossWorktrees, or something extensible like chat.historyScope: "repository" | "workspace"?
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions