lynkr 9.14.14 → 9.14.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -145,6 +145,9 @@ const STRICT = process.env.LYNKR_EMBEDDINGS_STRICT === 'true';
145
145
  function _noteFallback(providerName, err) {
146
146
  fallbackCount += 1;
147
147
  lastProviderError = err?.message || String(err);
148
+ // Cooldown counts from the LAST failed dial, not the first attempt's start —
149
+ // the in-place retry delay must not eat into the cooldown window.
150
+ lastProviderAttempt = Date.now();
148
151
  if (embeddingProviderAvailable !== false) {
149
152
  embeddingProviderAvailable = false;
150
153
  degradedSince = Date.now();
@@ -168,6 +171,14 @@ function _noteRecovery(providerName) {
168
171
  degradedSince = null;
169
172
  }
170
173
 
174
+ // One in-place retry before tripping degradation: a single transient failure
175
+ // (Ollama cold-loading the model at boot, a blip mid-restart) was flipping
176
+ // the WHOLE provider to hash embeddings for a full cooldown after every
177
+ // server start (recurring live incident, last 2026-09-26). A short backoff
178
+ // absorbs the blip; a provider that is actually down fails twice and
179
+ // degrades exactly as before.
180
+ const TRANSIENT_RETRY_DELAY_MS = 1500;
181
+
171
182
  function _wrapProvider(providerName, providerFn) {
172
183
  return async (text) => {
173
184
  // While degraded, only re-attempt the provider after the cooldown; serve
@@ -184,9 +195,25 @@ function _wrapProvider(providerName, providerFn) {
184
195
  const result = await providerFn(text);
185
196
  _noteRecovery(providerName);
186
197
  return result;
187
- } catch (err) {
188
- _noteFallback(providerName, err);
189
- if (STRICT) throw err;
198
+ } catch (firstErr) {
199
+ // Already-degraded providers get no second chance (this attempt WAS the
200
+ // post-cooldown probe); healthy ones earn one retry before the flip.
201
+ if (embeddingProviderAvailable !== false) {
202
+ await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS));
203
+ try {
204
+ const result = await providerFn(text);
205
+ _noteRecovery(providerName);
206
+ logger.debug({ provider: providerName, error: firstErr?.message },
207
+ '[Embeddings] Transient provider failure absorbed by retry');
208
+ return result;
209
+ } catch (secondErr) {
210
+ _noteFallback(providerName, secondErr);
211
+ if (STRICT) throw secondErr;
212
+ return generateHashEmbedding(text);
213
+ }
214
+ }
215
+ _noteFallback(providerName, firstErr);
216
+ if (STRICT) throw firstErr;
190
217
  return generateHashEmbedding(text);
191
218
  }
192
219
  };