diff --git a/lib/recall.js b/lib/recall.js index 2ccca02..8915fe3 100644 --- a/lib/recall.js +++ b/lib/recall.js @@ -811,6 +811,20 @@ export function segmentRecallBudget(hits, options = {}) { const average = costs.reduce((sum, cost) => sum + cost, 0) / costs.length const count = Math.max(1, Math.min(topK, Math.floor(budget / Math.max(1, average)))) const retained = rows.slice(0, count) + // ★ `slice` copies indexed elements only. `degraded` / `degradedError` / + // `skipped` are NON-enumerable by design (so the phase-A golden fixture and + // every serialised log keep their shape), which means a plain slice silently + // drops them — and the degradation notice would then never reach the model. + // Re-attach every out-of-band channel the recall path is known to carry. + for (const key of ['degraded', 'degradedError', 'skipped', 'skippedDim']) { + if (Object.prototype.hasOwnProperty.call(rows, key)) { + Object.defineProperty(retained, key, { + value: rows[key], + enumerable: false, + configurable: true, + }) + } + } const singleHitOverBudget = costs[0] > budget Object.defineProperty(retained, "budgetNotice", { value: singleHitOverBudget ? `[Memory] Top note estimate ${costs[0]} tokens exceeds memory budget ${budget}; retained without truncation.` : count < rows.length ? `[Memory] Recall limited to ${count} note(s) by memory budget ${budget}.` : "", enumerable: false }) return { hits: retained, budgetTokens: budget, source, singleHitOverBudget }