Phase 26 — Mem0 upsert + Letta geo hot cache
Closes the two remaining 2026-era memory findings. Both are
optimizations per J's framing — not load-bearing, but good data
hygiene + future-proofing at scale.
MEM0 UPSERT (data hygiene):
Before: /seed always appended. A scenario re-running the same
operation on the same day wrote duplicate entries, inflating the
playbook corpus with near-identical rows.
Now: upsert_entry(new) inspects existing non-retired entries and
decides ADD / UPDATE / NOOP:
ADD → no matching (operation, day, city, state) tuple, append
UPDATE → match exists with different names → merge (union, stable
order), refresh timestamp, keep original playbook_id so
citations stay valid
NOOP → match exists with identical names → skip, return id
Day-granularity keying on timestamp YYYY-MM-DD means intraday
re-seeds dedup but tomorrow's same-operation is a fresh ADD. Retired
entries don't block new seeds — they're out of scope anyway.
Seed endpoint returns {outcome: {mode, playbook_id, merged_names?},
entries_after}. Append=false retains old replace-all semantics.
5 unit tests pass: first_seed_is_add, identical_reseed_is_noop,
same_day_different_names_updates_and_merges, different_day_same_op_is_add,
retired_entry_doesnt_block_new_seed.
Live verified: three successive seeds with (Alice), (Alice),
(Alice, Bob) left entry count unchanged at 1936 with merged names
{Alejandro, Lauren, Alice, Bob}. Previously would have been 3
appends.
LETTA GEO HOT CACHE (scale primitive):
Added geo_index: HashMap<(city_lower, state_upper), Vec<usize>>
alongside PlaybookMemoryState. Rebuilt on every mutation: set_entries,
retire_one, retire_on_schema_drift, upsert_entry, load_from_storage.
compute_boost_for_filtered_with_role now uses the index for O(1) geo
lookup instead of scanning all entries. At current scale (1.9K) the
scan was sub-ms; at 100K+ the scan becomes the dominant cost. The
hot cache future-proofs without adding an LRU abstraction.
Retired entries excluded from index; valid_until still checked on the
hot path since it can elapse between rebuilds.
Owns cloned PlaybookEntries in the geo_filtered vector so the state
read-lock is released before cosine scoring — avoids lock contention
on the scoring path.
Memory-findings progress: 5 of 5 shipped.
✓ Multi-strategy parallel retrieval (Phase 19 refinement)
✓ Input normalization + unified /memory/query (Phase 24 TS)
✓ Zep validity windows (Phase 25)
✓ Mem0 UPSERT (Phase 26 today)
✓ Letta geo hot cache (Phase 26 today)
All 18 playbook_memory tests pass.
This commit is contained in:
@@ -2289,16 +2289,30 @@ async fn seed_playbook_memory(
|
||||
retirement_reason: None,
|
||||
};
|
||||
|
||||
let mut current = state.playbook_memory.snapshot().await;
|
||||
// Phase 26 — when append=true (default), route through upsert so
|
||||
// same-day re-seeds of the same operation merge instead of
|
||||
// appending duplicates. When append=false, retain the old
|
||||
// replace-all semantics for callers that want a hard reset.
|
||||
if req.append {
|
||||
current.push(new_entry);
|
||||
match state.playbook_memory.upsert_entry(new_entry).await {
|
||||
Ok(outcome) => {
|
||||
let entries_after = state.playbook_memory.entry_count().await;
|
||||
Ok(Json(serde_json::json!({
|
||||
"outcome": outcome,
|
||||
"entries_after": entries_after,
|
||||
})))
|
||||
}
|
||||
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, format!("upsert: {e}"))),
|
||||
}
|
||||
} else {
|
||||
current = vec![new_entry];
|
||||
if let Err(e) = state.playbook_memory.set_entries(vec![new_entry]).await {
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("persist: {e}")));
|
||||
}
|
||||
Ok(Json(serde_json::json!({
|
||||
"outcome": { "mode": "replaced", "playbook_id": pid },
|
||||
"entries_after": state.playbook_memory.entry_count().await,
|
||||
})))
|
||||
}
|
||||
if let Err(e) = state.playbook_memory.set_entries(current).await {
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("persist: {e}")));
|
||||
}
|
||||
Ok(Json(serde_json::json!({ "playbook_id": pid, "entries_after": state.playbook_memory.entry_count().await })))
|
||||
}
|
||||
|
||||
async fn rebuild_playbook_memory(
|
||||
|
||||
Reference in New Issue
Block a user