|
17 | 17 | (io/file (System/getProperty "user.home") ".cache"))] |
18 | 18 | (io/file cache-home "eca"))) |
19 | 19 |
|
| 20 | +(defn ^:private sorted-workspace-paths |
| 21 | + "Absolute workspace paths, de-duplicated and sorted, so the result is stable |
| 22 | + regardless of the order the editor reports its workspace folders." |
| 23 | + [workspaces uri->filename-fn] |
| 24 | + (->> workspaces |
| 25 | + (map #(str (fs/absolutize (fs/file (uri->filename-fn (:uri %)))))) |
| 26 | + (distinct) |
| 27 | + (sort))) |
| 28 | + |
20 | 29 | (defn workspaces-hash |
21 | | - "Returns an 8-char base64 (URL-safe, no padding) hash key for the given workspace set." |
| 30 | + "Returns an 8-char base64 (URL-safe, no padding) hash key for the given workspace set. |
| 31 | + Order-independent: the same set of folders always yields the same hash." |
22 | 32 | [workspaces uri->filename-fn] |
23 | | - (let [paths (->> workspaces |
24 | | - (map #(str (fs/absolutize (fs/file (uri->filename-fn (:uri %)))))) |
25 | | - (distinct) |
26 | | - (sort)) |
27 | | - joined (string/join ":" paths) |
| 33 | + (let [joined (string/join ":" (sorted-workspace-paths workspaces uri->filename-fn)) |
28 | 34 | md (java.security.MessageDigest/getInstance "SHA-256") |
29 | 35 | digest (.digest (doto md (.update (.getBytes joined "UTF-8")))) |
30 | 36 | encoder (-> (java.util.Base64/getUrlEncoder) |
|
38 | 44 |
|
39 | 45 | (defn ^:private workspace-dir-name |
40 | 46 | "Returns a human-readable directory name for the workspace cache. |
41 | | - Format: <sanitized-project-name>_<hash>, or just <hash> if no name is available." |
| 47 | + Format: <sanitized-project-name>_<hash>, or just <hash> if no name is available. |
| 48 | + The prefix comes from the sorted-first workspace path so it stays stable |
| 49 | + regardless of folder order; it is purely cosmetic - the <hash> is the identity." |
42 | 50 | [workspaces uri->filename-fn] |
43 | 51 | (let [hash (workspaces-hash workspaces uri->filename-fn) |
44 | | - first-uri (some-> workspaces first :uri) |
45 | | - project-name (when first-uri |
46 | | - (some-> (uri->filename-fn first-uri) |
47 | | - fs/file-name |
48 | | - str |
49 | | - not-empty)) |
| 52 | + project-name (some-> (first (sorted-workspace-paths workspaces uri->filename-fn)) |
| 53 | + fs/file-name |
| 54 | + str |
| 55 | + not-empty) |
50 | 56 | sanitized (when project-name |
51 | 57 | (let [s (string/replace project-name #"[^a-zA-Z0-9._-]" "_")] |
52 | 58 | (subs s 0 (min max-prefix-length (count s)))))] |
53 | 59 | (if (not-empty sanitized) |
54 | 60 | (str sanitized "_" hash) |
55 | 61 | hash))) |
56 | 62 |
|
57 | | -(defn ^:private migrate-workspace-cache-dir! |
58 | | - "Migrates old hash-only workspace cache directory to new human-readable format." |
59 | | - [^File old-dir ^File new-dir] |
60 | | - (try |
61 | | - (fs/move old-dir new-dir) |
62 | | - (logger/info logger-tag (str "Migrated workspace cache from " old-dir " to " new-dir)) |
63 | | - (catch Exception e |
64 | | - (logger/warn logger-tag "Failed to migrate workspace cache directory:" (.getMessage e))))) |
65 | | - |
66 | 63 | (defn workspace-cache-file |
67 | | - "Returns a File object for a workspace-specific cache file." |
| 64 | + "Returns a File object for a workspace-specific cache file. |
| 65 | + The directory identity is the order-independent <hash>; the human-readable |
| 66 | + prefix is cosmetic. Healing of caches fragmented across differently-named |
| 67 | + dirs for the same workspace is handled by eca.db/consolidate-workspace-cache!." |
68 | 68 | [workspaces filename uri->filename-fn] |
69 | | - (let [dir-name (workspace-dir-name workspaces uri->filename-fn) |
70 | | - hash-only (workspaces-hash workspaces uri->filename-fn) |
71 | | - base (global-dir) |
72 | | - new-dir (io/file base dir-name) |
73 | | - old-dir (io/file base hash-only)] |
74 | | - (when (and (not= dir-name hash-only) |
75 | | - (not (fs/exists? new-dir)) |
76 | | - (fs/exists? old-dir)) |
77 | | - (migrate-workspace-cache-dir! old-dir new-dir)) |
78 | | - (io/file new-dir filename))) |
| 69 | + (io/file (global-dir) (workspace-dir-name workspaces uri->filename-fn) filename)) |
| 70 | + |
| 71 | +(defn redundant-workspace-cache-files |
| 72 | + "Returns the cache files named `filename` that live in directories belonging to |
| 73 | + the same workspace set as the canonical dir but under a different name - i.e. |
| 74 | + legacy hash-only dirs, or dirs prefixed from a different folder order. The |
| 75 | + canonical dir is excluded. Used to heal fragmented chat caches." |
| 76 | + [workspaces filename uri->filename-fn] |
| 77 | + (let [hash (workspaces-hash workspaces uri->filename-fn) |
| 78 | + canonical-dir-name (workspace-dir-name workspaces uri->filename-fn) |
| 79 | + base (global-dir)] |
| 80 | + (if (fs/exists? base) |
| 81 | + (->> (fs/list-dir base) |
| 82 | + (filter fs/directory?) |
| 83 | + (map #(str (fs/file-name %))) |
| 84 | + (filter (fn [n] (or (= n hash) |
| 85 | + (string/ends-with? n (str "_" hash))))) |
| 86 | + (remove #(= % canonical-dir-name)) |
| 87 | + (map #(io/file base % filename)) |
| 88 | + (filter fs/exists?) |
| 89 | + (vec)) |
| 90 | + []))) |
79 | 91 |
|
80 | 92 | (def ^:private tool-call-outputs-dir-name "toolCallOutputs") |
81 | 93 | (def ^:private plugins-dir-name "plugins") |
|
0 commit comments