Store records the response for the given question, replacing any existing entry with the same normalized key. Storing the same (question, response) pair twice is a no-op and skips the file rewrite — useful when an agent's stop hook re-fires with the same content (e.g. a cache-replay turn). When the
(question, response string)
| 133 | // processes simultaneously storing different keys both see their |
| 134 | // writes preserved on disk; in-process callers serialize via c.mu. |
| 135 | func (c *Cache) Store(question, response string) { |
| 136 | key := c.normalize(question) |
| 137 | |
| 138 | c.mu.Lock() |
| 139 | defer c.mu.Unlock() |
| 140 | |
| 141 | // Cheap in-memory dedup: skip the cross-process lock and disk |
| 142 | // write when our local view already matches. After a recent |
| 143 | // Lookup our view is fresh, so this catches the common |
| 144 | // "store the answer we just replayed" path of cache_response. |
| 145 | if existing, ok := c.entries[key]; ok && existing == response { |
| 146 | return |
| 147 | } |
| 148 | |
| 149 | if c.path != "" { |
| 150 | if err := c.persistToDisk(key, response); err != nil { |
| 151 | // Persistence failures are not fatal: keep the entry |
| 152 | // in memory and let the next Store retry the file |
| 153 | // write. |
| 154 | slog.Warn("cache persist failed; keeping entry in memory only", |
| 155 | "path", c.path, "error", err) |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | // Update in-memory map after persist (not before) so that if persist |
| 160 | // fails, we still have the entry in memory for this process. The next |
| 161 | // Lookup will reload from disk if another process wrote successfully. |
| 162 | |
| 163 | c.entries[key] = response |
| 164 | } |
| 165 | |
| 166 | // persistToDisk takes the cross-process lock on c.path's sibling .lock |
| 167 | // file, reloads the on-disk entries, merges (key, response), writes |