[BUG] bad_recall_count accumulates permanently — last_confirmed_use_at never written causes permanent memory suppression
Plugin Version
1.1.0-beta.10 (latest commit, post-PR #354 merged)
Bug Description
After PR #354 fixed the state: "pending" deadlock (auto-capture now writes state: "confirmed"), a second governance filter issue remains: bad_recall_count accumulates without bound and last_confirmed_use_at is never written, causing memories to be permanently suppressed after 3 consecutive injections.
Relationship with PR #354
PR #354 scope: Fixed state: "pending" → state: "confirmed" in all 5 auto-capture write paths (index.ts regex fallback + smart-extractor.ts 4 paths). This resolved Issue #350's deadlock.
This bug is separate and was NOT covered by PR #354:
Root Cause
The feedback loop that controls suppressed_until_turn has a critical gap:
// index.ts:2559-2565
const staleInjected =
typeof meta.last_confirmed_use_at === "number" && // ← ALWAYS false
meta.last_injected_at > 0 &&
(
typeof meta.last_confirmed_use_at !== "number" || // ← ALWAYS true
meta.last_confirmed_use_at < meta.last_injected_at
);
last_confirmed_use_at is never written anywhere in the codebase (verified by searching the entire repository). As a result:
typeof meta.last_confirmed_use_at === "number" → always false
staleInjected → evaluates to false (due to first condition being false, short-circuit)
- The code treats
staleInjected: false as "good recall" — BUT the counter never resets either
The critical issue: on the first-ever injection of a memory, staleInjected is true (because last_confirmed_use_at is undefined, making the entire expression evaluate to true through the last disjunct). This causes bad_recall_count to immediately jump to the suppression threshold.
Suppression Logic
// index.ts:2566-2569
const nextBadRecallCount = staleInjected
? meta.bad_recall_count + 1 // ← Increments on every injection (first injection: 0→3)
: meta.bad_recall_count; // ← Never resets because staleInjected is always true
const shouldSuppress = nextBadRecallCount >= 3 && minRepeated > 0;
// Once bad_recall_count reaches 3, shouldSuppress = true forever
// index.ts:2432-2435
if (meta.suppressed_until_turn > 0 && currentTurn <= meta.suppressed_until_turn) {
suppressedFilteredCount++;
return false; // Permanently filtered once bad_recall_count >= 3
}
Expected vs Actual Behavior
| Turn |
Expected |
Actual |
| Turn 1 |
Memory injected, bad_recall_count = 0 |
✅ Correct (injected) |
| Turn 2 |
Memory injected, bad_recall_count = 0 |
❌ Suppressed (bad_recall_count jumped to 3 on first injection) |
| Turn 3+ |
Memory injected |
❌ Still suppressed until turn > suppressed_until_turn |
The first-ever injection of a memory is always treated as "stale" because last_confirmed_use_at is undefined, causing bad_recall_count to immediately reach the suppression threshold.
Symptoms Observed
Gateway log:
memory-lancedb-pro: auto-recall skipped after governance filters (hits=6, dedupFiltered=0, stateFiltered=0, suppressedFiltered=6)
hits=6: 6 memories found by semantic search
suppressedFiltered=6: ALL 6 are permanently suppressed
- Pattern: After Gateway restart, the first recall in each session finds memories, but they are all suppressed
Turn Counter Behavior
The turnCounter is a module-level Map that resets on Gateway restart:
// index.ts:2322
const currentTurn = (turnCounter.get(sessionId) || 0) + 1;
This means after restart:
- Turn 1: Memory injected →
bad_recall_count becomes 3 → suppressed_until_turn set to 1 + minRepeated (e.g., 9)
- Turns 2-9: Suppressed
- Turn 10+: Suppression expires (turn > suppressed_until_turn) → Memory appears once → cycle repeats
This explains the "appears once after restart" behavior reported by users.
Steps to Reproduce
- Enable
autoCapture: true and autoRecall: true
- Have a conversation that triggers auto-capture
- Restart the Gateway
- In the new session, observe: memory appears on the first recall
- All subsequent recalls in the same session: memory is suppressed
- After
minRepeated (default 8) turns pass, memory appears again for exactly one recall, then is suppressed again
Root Cause Analysis
The bad_recall_count / suppressed_until_turn mechanism was designed to work with last_confirmed_use_at to form a feedback loop:
good recall → last_confirmed_use_at updated → staleInjected=false → bad_recall_count resets
bad recall → bad_recall_count increments
bad_recall_count >= 3 → suppress
But since last_confirmed_use_at is never written, the "good recall" path never executes:
staleInjected is always true on first-ever injection (undefined field)
bad_recall_count immediately reaches 3 after first injection
shouldSuppress = true forever
- The only escape:
currentTurn > suppressed_until_turn expires after minRepeated turns
Proposed Fixes
Fix A (Minimal, recommended): Set autoRecallMinRepeated: 0
As a config workaround, setting autoRecallMinRepeated: 0 disables the suppression mechanism entirely:
{
"plugins": {
"entries": {
"memory-lancedb-pro": {
"config": {
"autoRecallMinRepeated": 0
}
}
}
}
}
This is a valid workaround since the suppression mechanism is broken anyway.
Fix B (Code fix): Wire up last_confirmed_use_at
The proper fix requires:
-
Write last_confirmed_use_at after confirmed usage:
- In
agent_end hook, compare recalled memories against actual agent response
- If memory content was used → write
last_confirmed_use_at = Date.now()
-
Reset bad_recall_count when last_confirmed_use_at >= last_injected_at:
- Change the condition so that confirmed use resets the counter
-
Alternative: Remove last_confirmed_use_at entirely and use a simpler decrement strategy:
- On each successful injection without confirmed use:
bad_recall_count = min(bad_recall_count + 1, 3)
- On each turn that passes without injection:
bad_recall_count = max(0, bad_recall_count - 1)
- This naturally decays the counter without needing a confirmed-use signal
Impact
- Any user with
autoRecallMinRepeated > 0 (default: 8) experiences broken auto-recall
- Memories appear only once per session/restart cycle, then disappear for the next 8 turns
- This affects both auto-captured and manually stored memories
- The suppression mechanism, designed to prevent noisy repeated memories, instead causes the opposite: memories disappearing when they should be available
Workaround
Until a fix is released, add to plugin config:
"autoRecallMinRepeated": 0
This disables suppression and restores normal recall behavior, at the cost of losing the "suppress recent repeats" feature (which is non-functional anyway due to this bug).
Related
[BUG]
bad_recall_countaccumulates permanently —last_confirmed_use_atnever written causes permanent memory suppressionPlugin Version
1.1.0-beta.10 (latest commit, post-PR #354 merged)
Bug Description
After PR #354 fixed the
state: "pending"deadlock (auto-capture now writesstate: "confirmed"), a second governance filter issue remains:bad_recall_countaccumulates without bound andlast_confirmed_use_atis never written, causing memories to be permanently suppressed after 3 consecutive injections.Relationship with PR #354
PR #354 scope: Fixed
state: "pending"→state: "confirmed"in all 5 auto-capture write paths (index.ts regex fallback + smart-extractor.ts 4 paths). This resolved Issue #350's deadlock.This bug is separate and was NOT covered by PR #354:
state: "confirmed"writes and governance filter behavior forstatebad_recall_count,suppressed_until_turn, orlast_confirmed_use_atbad_recall_countpermanent suppression bug existed before PR fix: auto-captured memories write confirmed state to unblock autoRecall #354 and remained after it was mergedRoot Cause
The feedback loop that controls
suppressed_until_turnhas a critical gap:last_confirmed_use_atis never written anywhere in the codebase (verified by searching the entire repository). As a result:typeof meta.last_confirmed_use_at === "number"→ alwaysfalsestaleInjected→ evaluates tofalse(due to first condition being false, short-circuit)staleInjected: falseas "good recall" — BUT the counter never resets eitherThe critical issue: on the first-ever injection of a memory,
staleInjectedistrue(becauselast_confirmed_use_atisundefined, making the entire expression evaluate totruethrough the last disjunct). This causesbad_recall_countto immediately jump to the suppression threshold.Suppression Logic
Expected vs Actual Behavior
The first-ever injection of a memory is always treated as "stale" because
last_confirmed_use_atis undefined, causingbad_recall_countto immediately reach the suppression threshold.Symptoms Observed
Gateway log:
hits=6: 6 memories found by semantic searchsuppressedFiltered=6: ALL 6 are permanently suppressedTurn Counter Behavior
The
turnCounteris a module-level Map that resets on Gateway restart:This means after restart:
bad_recall_countbecomes 3 →suppressed_until_turnset to1 + minRepeated(e.g., 9)This explains the "appears once after restart" behavior reported by users.
Steps to Reproduce
autoCapture: trueandautoRecall: trueminRepeated(default 8) turns pass, memory appears again for exactly one recall, then is suppressed againRoot Cause Analysis
The
bad_recall_count/suppressed_until_turnmechanism was designed to work withlast_confirmed_use_atto form a feedback loop:But since
last_confirmed_use_atis never written, the "good recall" path never executes:staleInjectedis alwaystrueon first-ever injection (undefined field)bad_recall_countimmediately reaches 3 after first injectionshouldSuppress = trueforevercurrentTurn > suppressed_until_turnexpires afterminRepeatedturnsProposed Fixes
Fix A (Minimal, recommended): Set
autoRecallMinRepeated: 0As a config workaround, setting
autoRecallMinRepeated: 0disables the suppression mechanism entirely:{ "plugins": { "entries": { "memory-lancedb-pro": { "config": { "autoRecallMinRepeated": 0 } } } } }This is a valid workaround since the suppression mechanism is broken anyway.
Fix B (Code fix): Wire up
last_confirmed_use_atThe proper fix requires:
Write
last_confirmed_use_atafter confirmed usage:agent_endhook, compare recalled memories against actual agent responselast_confirmed_use_at = Date.now()Reset
bad_recall_countwhenlast_confirmed_use_at >= last_injected_at:Alternative: Remove
last_confirmed_use_atentirely and use a simpler decrement strategy:bad_recall_count = min(bad_recall_count + 1, 3)bad_recall_count = max(0, bad_recall_count - 1)Impact
autoRecallMinRepeated > 0(default: 8) experiences broken auto-recallWorkaround
Until a fix is released, add to plugin config:
This disables suppression and restores normal recall behavior, at the cost of losing the "suppress recent repeats" feature (which is non-functional anyway due to this bug).
Related
pendingstate, blocked from auto-recall by governance filter #350 (fixed by PR fix: auto-captured memories write confirmed state to unblock autoRecall #354):state: "pending"deadlock — same governance filter, different fieldstate: "pending"→state: "confirmed". Did NOT addressbad_recall_count/last_confirmed_use_at