(ctx context.Context)
| 167 | } |
| 168 | |
| 169 | func (mw *memoryWorker) maybeExtract(ctx context.Context) { |
| 170 | mw.mu.Lock() |
| 171 | lastIdx := mw.lastProcessedIdx |
| 172 | mw.mu.Unlock() |
| 173 | |
| 174 | historyLen := len(mw.cli.history) |
| 175 | newMessages := historyLen - lastIdx |
| 176 | |
| 177 | // One gate for back-pressure, cadence (cooldown + min new messages) and the |
| 178 | // circuit breaker. Only release when it actually acquired. |
| 179 | if !mw.coord.tryAcquire(newMessages) { |
| 180 | return |
| 181 | } |
| 182 | defer mw.coord.release() |
| 183 | |
| 184 | // Extract the new messages (copy to avoid races) |
| 185 | messagesToProcess := make([]models.Message, newMessages) |
| 186 | copy(messagesToProcess, mw.cli.history[lastIdx:]) |
| 187 | |
| 188 | mw.logger.Debug("Memory worker: extracting annotations", |
| 189 | zap.Int("new_messages", newMessages), |
| 190 | zap.Int("from_idx", lastIdx), |
| 191 | ) |
| 192 | |
| 193 | // Record interaction event |
| 194 | if mw.cli.memoryStore != nil { |
| 195 | mw.cli.memoryStore.RecordInteraction(memory.InteractionEvent{ |
| 196 | Timestamp: time.Now(), |
| 197 | Feature: mw.detectFeature(), |
| 198 | }) |
| 199 | } |
| 200 | |
| 201 | // Show subtle status |
| 202 | mw.showStatus("updating memory...") |
| 203 | |
| 204 | // Queued segments first (oldest dialog wins on causality), then the live one. |
| 205 | mw.drainPending(ctx) |
| 206 | |
| 207 | err := mw.extractAndSave(ctx, messagesToProcess) |
| 208 | |
| 209 | if err != nil { |
| 210 | mw.onExtractionFailure(err, messagesToProcess, historyLen) |
| 211 | } else { |
| 212 | mw.onExtractionSuccess(historyLen) |
| 213 | } |
| 214 | |
| 215 | mw.clearStatus() |
| 216 | } |
| 217 | |
| 218 | // onExtractionSuccess advances the watermark, resets the failure streak and |
| 219 | // invalidates the prompt cache so the next turn sees the new memory. |
no test coverage detected