perf(consolidation): write only decayed rows that changed (#1417)
* perf(consolidation-pipeline): skip decay writes for unchanged memories The decay tier of mem::consolidate-pipeline listed every semantic and procedural memory and wrote every row back on every run, even when applyDecay left the strength untouched (the common case for anything not yet due for decay). Each write is a full state::set RPC and marks the whole scope dirty for the engine's next flush, so this scaled with total memory count on every consolidation cycle regardless of how much actually decayed. Capture each item's strength before calling applyDecay and only call kv.set for items whose strength changed by more than a small float tolerance, mirroring the dirty-only pattern already used by mem::lesson-decay-sweep. applyDecay itself is unchanged, so the decay trajectory over time is identical to before; only the redundant no-op writes for untouched rows are removed. results.decay now reports scanned/written counts per tier instead of just the row count. Added tests covering: unchanged rows are not written, changed rows are written with correct scanned/written counts, and decay progression across simulated time lands at the same strengths as the unconditional write path did. * perf(consolidation): write decayed rows one at a time The first decay run after an upgrade can find most rows due at once. Writing them all in parallel would fire that many state::set calls at the engine together, where they queue behind its single store lock. Sequential writes keep the load the same shape as before. * test(consolidation-pipeline): assert persistence at each decay step The simulated-time decay test only checked kv.list()[0].strength, but mockKV.list returns live object references that applyDecay mutates in place. That let the assertions pass even if a kv.set write were skipped, since the in-memory object already reflected the mutation regardless of persistence. Spy on kv.set and assert exactly one semantic write after the 45-day step and a second after the 95-day step, so a missed persistence write would actually fail the test. * fix(consolidation): decay at most once per period applyDecay recomputed daysSince from lastAccessedAt on every pipeline run, and lastAccessedAt never advances, so each run re-applied the 0.9^periods factor to the already-decayed strength. With the default consolidation cooldown this compounded a month-old fact down to the 0.1 floor within hours of active use instead of over months. Track lastDecayedAt on semantic and procedural rows and anchor the next decay check to max(lastAccessedAt, lastDecayedAt), only applying another decay step once decayDays have passed since that anchor. Rows without the field decay once, same as before, and get it set. Updated the simulated-time test to assert once-per-period decay (including a run inside the same period that must not write) instead of pinning the old compounded value.
R
Rohit Ghumare committed
e69a606e8faf5ccdde15723278a8bd7dd7ec047a
Parent: bcf4f0d
Committed by GitHub <noreply@github.com>
on 9/28/2026, 6:38:00 AM