chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:34 +08:00
commit a789495a98
1551 changed files with 718128 additions and 0 deletions
+399
View File
@@ -0,0 +1,399 @@
use super::*;
use crate::memory_types::PipelineState;
use std::sync::Mutex;
use std::time::Duration;
/// Global memory activity state - updated by sidecar, read by info widget
static MEMORY_ACTIVITY: Mutex<Option<MemoryActivity>> = Mutex::new(None);
/// Maximum number of recent events to keep
const MAX_RECENT_EVENTS: usize = 10;
/// Staleness timeout: auto-reset to Idle if state has been non-Idle for this long
const STALENESS_TIMEOUT_SECS: u64 = 10;
/// Get current memory activity state
pub fn get_activity() -> Option<MemoryActivity> {
MEMORY_ACTIVITY.lock().ok().and_then(|guard| guard.clone())
}
pub fn activity_snapshot() -> Option<crate::protocol::MemoryActivitySnapshot> {
get_activity().as_ref().map(memory_activity_snapshot)
}
pub fn apply_remote_activity_snapshot(snapshot: &crate::protocol::MemoryActivitySnapshot) {
if let Ok(mut guard) = MEMORY_ACTIVITY.lock() {
let recent_events = guard
.as_ref()
.map(|activity| activity.recent_events.clone())
.unwrap_or_default();
let now = Instant::now();
let state_since = now
.checked_sub(Duration::from_millis(snapshot.state_age_ms))
.unwrap_or(now);
*guard = Some(MemoryActivity {
state: from_snapshot_state(&snapshot.state),
state_since,
pipeline: snapshot.pipeline.as_ref().map(from_snapshot_pipeline),
recent_events,
});
}
}
/// Update the memory activity state
pub fn set_state(state: MemoryState) {
if let Ok(mut guard) = MEMORY_ACTIVITY.lock() {
if let Some(activity) = guard.as_mut() {
activity.state = state;
activity.state_since = Instant::now();
} else {
*guard = Some(MemoryActivity {
state,
state_since: Instant::now(),
pipeline: None,
recent_events: Vec::new(),
});
}
}
}
/// Add an event to the activity log
pub fn add_event(kind: MemoryEventKind) {
crate::memory_log::log_event(&kind);
if let Ok(mut guard) = MEMORY_ACTIVITY.lock() {
let event = MemoryEvent {
kind,
timestamp: Instant::now(),
detail: None,
};
if let Some(activity) = guard.as_mut() {
activity.recent_events.insert(0, event);
activity.recent_events.truncate(MAX_RECENT_EVENTS);
} else {
*guard = Some(MemoryActivity {
state: MemoryState::Idle,
state_since: Instant::now(),
pipeline: None,
recent_events: vec![event],
});
}
}
}
/// Start a new pipeline run (called at the beginning of each memory check)
pub fn pipeline_start() {
if let Ok(mut guard) = MEMORY_ACTIVITY.lock() {
if let Some(activity) = guard.as_mut() {
activity.pipeline = Some(PipelineState::new());
} else {
*guard = Some(MemoryActivity {
state: MemoryState::Idle,
state_since: Instant::now(),
pipeline: Some(PipelineState::new()),
recent_events: Vec::new(),
});
}
}
}
/// Update pipeline step status
#[expect(
clippy::collapsible_if,
reason = "Memory activity updates keep optional state transitions explicit"
)]
pub fn pipeline_update(f: impl FnOnce(&mut PipelineState)) {
if let Ok(mut guard) = MEMORY_ACTIVITY.lock() {
if let Some(activity) = guard.as_mut() {
if let Some(pipeline) = activity.pipeline.as_mut() {
f(pipeline);
}
}
}
}
/// Check for staleness and auto-reset if needed.
/// Returns true if state was reset due to staleness.
#[expect(
clippy::collapsible_if,
reason = "Memory activity timeout checks keep nested optional state explicit"
)]
pub fn check_staleness() -> bool {
if let Ok(mut guard) = MEMORY_ACTIVITY.lock() {
if let Some(activity) = guard.as_mut() {
if !matches!(activity.state, MemoryState::Idle)
&& activity.state_since.elapsed().as_secs() >= STALENESS_TIMEOUT_SECS
{
crate::logging::info(&format!(
"Memory state stale ({:?} for {}s), auto-resetting to Idle",
activity.state,
activity.state_since.elapsed().as_secs()
));
activity.state = MemoryState::Idle;
activity.state_since = Instant::now();
return true;
}
}
}
false
}
/// Clear activity (reset to idle with no events)
pub fn clear_activity() {
if let Ok(mut guard) = MEMORY_ACTIVITY.lock() {
*guard = None;
}
}
/// Record that a memory payload was injected into model context.
/// This feeds the memory info widget with injected content + metadata.
pub fn record_injected_prompt(prompt: &str, count: usize, age_ms: u64) {
crate::telemetry::record_memory_injected(count, age_ms);
let items = parse_injected_items(prompt, 8);
let preview = prompt_preview(prompt, 72);
add_event(MemoryEventKind::MemoryInjected {
count,
prompt_chars: prompt.chars().count(),
age_ms,
preview: preview.clone(),
items,
});
add_event(MemoryEventKind::MemorySurfaced {
memory_preview: preview,
});
}
fn parse_injected_items(prompt: &str, max_items: usize) -> Vec<InjectedMemoryItem> {
let mut items: Vec<InjectedMemoryItem> = Vec::new();
let mut section = String::from("Memory");
for raw_line in prompt.lines() {
let line = raw_line.trim();
if line.is_empty() || line == "# Memory" {
continue;
}
if let Some(header) = line.strip_prefix("## ") {
let header = header.trim();
if !header.is_empty() {
section = header.to_string();
}
continue;
}
let content = if let Some(rest) = line.strip_prefix("- ") {
Some(rest.trim())
} else if let Some((prefix, rest)) = line.split_once(". ") {
if !prefix.is_empty() && prefix.chars().all(|c| c.is_ascii_digit()) {
Some(rest.trim())
} else {
None
}
} else {
None
};
if let Some(content) = content {
if content.is_empty() {
continue;
}
items.push(InjectedMemoryItem {
section: section.clone(),
content: content.to_string(),
});
if items.len() >= max_items {
return items;
}
}
}
if items.is_empty() {
let fallback = prompt
.lines()
.map(str::trim)
.filter(|line| {
!line.is_empty()
&& !line.starts_with('#')
&& !line.starts_with("## ")
&& !line.starts_with("- ")
})
.collect::<Vec<_>>()
.join(" ");
if !fallback.is_empty() {
items.push(InjectedMemoryItem {
section,
content: fallback,
});
}
}
items
}
fn prompt_preview(prompt: &str, max_chars: usize) -> String {
let bullet = prompt
.lines()
.map(str::trim)
.find_map(|line| {
if line.starts_with("- ") {
Some(line.trim_start_matches("- ").trim())
} else if let Some((prefix, rest)) = line.split_once(". ") {
if !prefix.is_empty() && prefix.chars().all(|c| c.is_ascii_digit()) {
Some(rest.trim())
} else {
None
}
} else {
None
}
})
.unwrap_or_else(|| prompt.trim());
if bullet.chars().count() <= max_chars {
bullet.to_string()
} else {
let mut out = String::new();
for (i, ch) in bullet.chars().enumerate() {
if i >= max_chars.saturating_sub(3) {
break;
}
out.push(ch);
}
out.push_str("...");
out
}
}
fn memory_activity_snapshot(activity: &MemoryActivity) -> crate::protocol::MemoryActivitySnapshot {
crate::protocol::MemoryActivitySnapshot {
state: snapshot_state(&activity.state),
state_age_ms: activity.state_since.elapsed().as_millis() as u64,
pipeline: activity.pipeline.as_ref().map(snapshot_pipeline),
}
}
fn snapshot_state(state: &MemoryState) -> crate::protocol::MemoryStateSnapshot {
match state {
MemoryState::Idle => crate::protocol::MemoryStateSnapshot::Idle,
MemoryState::Embedding => crate::protocol::MemoryStateSnapshot::Embedding,
MemoryState::SidecarChecking { count } => {
crate::protocol::MemoryStateSnapshot::SidecarChecking { count: *count }
}
MemoryState::FoundRelevant { count } => {
crate::protocol::MemoryStateSnapshot::FoundRelevant { count: *count }
}
MemoryState::Extracting { reason } => crate::protocol::MemoryStateSnapshot::Extracting {
reason: reason.clone(),
},
MemoryState::Maintaining { phase } => crate::protocol::MemoryStateSnapshot::Maintaining {
phase: phase.clone(),
},
MemoryState::ToolAction { action, detail } => {
crate::protocol::MemoryStateSnapshot::ToolAction {
action: action.clone(),
detail: detail.clone(),
}
}
}
}
fn snapshot_pipeline(pipeline: &PipelineState) -> crate::protocol::MemoryPipelineSnapshot {
crate::protocol::MemoryPipelineSnapshot {
search: snapshot_step_status(&pipeline.search),
search_result: pipeline.search_result.as_ref().map(snapshot_step_result),
verify: snapshot_step_status(&pipeline.verify),
verify_result: pipeline.verify_result.as_ref().map(snapshot_step_result),
verify_progress: pipeline.verify_progress,
inject: snapshot_step_status(&pipeline.inject),
inject_result: pipeline.inject_result.as_ref().map(snapshot_step_result),
maintain: snapshot_step_status(&pipeline.maintain),
maintain_result: pipeline.maintain_result.as_ref().map(snapshot_step_result),
}
}
fn snapshot_step_status(status: &StepStatus) -> crate::protocol::MemoryStepStatusSnapshot {
match status {
StepStatus::Pending => crate::protocol::MemoryStepStatusSnapshot::Pending,
StepStatus::Running => crate::protocol::MemoryStepStatusSnapshot::Running,
StepStatus::Done => crate::protocol::MemoryStepStatusSnapshot::Done,
StepStatus::Error => crate::protocol::MemoryStepStatusSnapshot::Error,
StepStatus::Skipped => crate::protocol::MemoryStepStatusSnapshot::Skipped,
}
}
fn snapshot_step_result(result: &StepResult) -> crate::protocol::MemoryStepResultSnapshot {
crate::protocol::MemoryStepResultSnapshot {
summary: result.summary.clone(),
latency_ms: result.latency_ms,
}
}
fn from_snapshot_state(snapshot: &crate::protocol::MemoryStateSnapshot) -> MemoryState {
match snapshot {
crate::protocol::MemoryStateSnapshot::Idle => MemoryState::Idle,
crate::protocol::MemoryStateSnapshot::Embedding => MemoryState::Embedding,
crate::protocol::MemoryStateSnapshot::SidecarChecking { count } => {
MemoryState::SidecarChecking { count: *count }
}
crate::protocol::MemoryStateSnapshot::FoundRelevant { count } => {
MemoryState::FoundRelevant { count: *count }
}
crate::protocol::MemoryStateSnapshot::Extracting { reason } => MemoryState::Extracting {
reason: reason.clone(),
},
crate::protocol::MemoryStateSnapshot::Maintaining { phase } => MemoryState::Maintaining {
phase: phase.clone(),
},
crate::protocol::MemoryStateSnapshot::ToolAction { action, detail } => {
MemoryState::ToolAction {
action: action.clone(),
detail: detail.clone(),
}
}
}
}
fn from_snapshot_pipeline(snapshot: &crate::protocol::MemoryPipelineSnapshot) -> PipelineState {
PipelineState {
search: from_snapshot_step_status(&snapshot.search),
search_result: snapshot
.search_result
.as_ref()
.map(from_snapshot_step_result),
verify: from_snapshot_step_status(&snapshot.verify),
verify_result: snapshot
.verify_result
.as_ref()
.map(from_snapshot_step_result),
verify_progress: snapshot.verify_progress,
inject: from_snapshot_step_status(&snapshot.inject),
inject_result: snapshot
.inject_result
.as_ref()
.map(from_snapshot_step_result),
maintain: from_snapshot_step_status(&snapshot.maintain),
maintain_result: snapshot
.maintain_result
.as_ref()
.map(from_snapshot_step_result),
started_at: Instant::now(),
}
}
fn from_snapshot_step_status(snapshot: &crate::protocol::MemoryStepStatusSnapshot) -> StepStatus {
match snapshot {
crate::protocol::MemoryStepStatusSnapshot::Pending => StepStatus::Pending,
crate::protocol::MemoryStepStatusSnapshot::Running => StepStatus::Running,
crate::protocol::MemoryStepStatusSnapshot::Done => StepStatus::Done,
crate::protocol::MemoryStepStatusSnapshot::Error => StepStatus::Error,
crate::protocol::MemoryStepStatusSnapshot::Skipped => StepStatus::Skipped,
}
}
fn from_snapshot_step_result(snapshot: &crate::protocol::MemoryStepResultSnapshot) -> StepResult {
StepResult {
summary: snapshot.summary.clone(),
latency_ms: snapshot.latency_ms,
}
}
+58
View File
@@ -0,0 +1,58 @@
use crate::memory_graph::MemoryGraph;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use std::time::SystemTime;
// === Graph Cache ===
struct GraphCacheEntry {
graph: MemoryGraph,
modified: Option<SystemTime>,
}
struct GraphCache {
entries: HashMap<PathBuf, GraphCacheEntry>,
}
impl GraphCache {
fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
}
static GRAPH_CACHE: OnceLock<Mutex<GraphCache>> = OnceLock::new();
fn graph_cache() -> &'static Mutex<GraphCache> {
GRAPH_CACHE.get_or_init(|| Mutex::new(GraphCache::new()))
}
fn graph_mtime(path: &PathBuf) -> Option<SystemTime> {
std::fs::metadata(path).ok().and_then(|m| m.modified().ok())
}
pub(super) fn cached_graph(path: &PathBuf) -> Option<MemoryGraph> {
let modified = graph_mtime(path);
let cache = graph_cache().lock().ok()?;
let entry = cache.entries.get(path)?;
if entry.modified == modified {
Some(entry.graph.clone())
} else {
None
}
}
pub(super) fn cache_graph(path: PathBuf, graph: &MemoryGraph) {
let modified = graph_mtime(&path);
if let Ok(mut cache) = graph_cache().lock() {
cache.entries.insert(
path,
GraphCacheEntry {
graph: graph.clone(),
modified,
},
);
}
}
+458
View File
@@ -0,0 +1,458 @@
use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::time::Instant;
type LastInjectedMemorySetBySession = HashMap<String, (HashSet<String>, Instant)>;
type InjectedMemoryIdsBySession = HashMap<String, HashMap<String, Instant>>;
/// Pending memory prompt from background check - ready to inject on next turn.
/// Keyed by session ID so each session gets its own pending memory.
static PENDING_MEMORY: Mutex<Option<HashMap<String, PendingMemory>>> = Mutex::new(None);
/// Signature of the last injected prompt to suppress near-immediate duplicates.
/// Keyed by session ID.
static LAST_INJECTED_PROMPT_SIGNATURE: Mutex<Option<HashMap<String, (String, Instant)>>> =
Mutex::new(None);
/// Recently injected memory ID sets per session.
/// Used to suppress near-duplicate re-injection even when formatting differs.
static LAST_INJECTED_MEMORY_SET: Mutex<Option<LastInjectedMemorySetBySession>> = Mutex::new(None);
/// Memory IDs that have already been injected into the conversation, with the
/// time they were injected. Used to prevent the same memory from being
/// re-injected on subsequent turns while it is still fresh in the transcript.
/// Keyed by session ID.
static INJECTED_MEMORY_IDS: Mutex<Option<InjectedMemoryIdsBySession>> = Mutex::new(None);
/// Guard to ensure only one memory check runs at a time, per session.
/// Keyed by session ID.
static MEMORY_CHECK_IN_PROGRESS: Mutex<Option<HashSet<String>>> = Mutex::new(None);
/// Suppress repeated identical memory payloads within this many seconds.
const MEMORY_REPEAT_SUPPRESSION_SECS: u64 = 90;
/// Suppress substantially overlapping memory sets for a bit longer.
const MEMORY_SET_REPEAT_SUPPRESSION_SECS: u64 = 180;
/// If a new pending payload overlaps this much with the last injected set,
/// treat it as too similar to surface again immediately.
const MEMORY_SET_OVERLAP_SUPPRESSION_RATIO: f32 = 0.8;
/// How long an injected memory counts as "already known" to a session.
///
/// Injection payloads are ephemeral (not persisted into history), but the
/// model's response that consumed them IS part of the transcript, so
/// re-injecting the same memory shortly afterwards is pure noise. This
/// tracking used to be cleared on every detected topic change, which fires
/// often on real sessions (cosine similarity between consecutive coding turns
/// regularly sits below the topic threshold), so the same memory could be
/// re-injected minutes apart in one transcript. A TTL keeps the dedup stable
/// across topic wobble while still letting genuinely old memories resurface in
/// long sessions once they may have scrolled out of (or been compacted from)
/// the context window.
const INJECTED_MEMORY_TTL_SECS: u64 = 45 * 60;
fn injected_recently(at: &Instant) -> bool {
at.elapsed().as_secs() < INJECTED_MEMORY_TTL_SECS
}
/// A pending memory result from async checking.
#[derive(Debug, Clone)]
pub struct PendingMemory {
/// The formatted memory prompt ready for injection.
pub prompt: String,
/// Optional UI-focused rendering of the injected memory payload.
/// This can contain extra display-only metadata that is not sent to the model.
pub display_prompt: Option<String>,
/// When this was computed.
pub computed_at: Instant,
/// Number of relevant memories found.
pub count: usize,
/// IDs of memories included in this prompt (for dedup tracking).
pub memory_ids: Vec<String>,
}
impl PendingMemory {
/// Check if this pending memory is still fresh (not too old).
pub fn is_fresh(&self) -> bool {
self.computed_at.elapsed().as_secs() < 120
}
}
fn prompt_signature(prompt: &str) -> String {
prompt
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
.to_lowercase()
}
fn memory_set(ids: &[String]) -> HashSet<String> {
ids.iter().cloned().collect()
}
fn memory_overlap_ratio(left: &HashSet<String>, right: &HashSet<String>) -> f32 {
if left.is_empty() || right.is_empty() {
return 0.0;
}
let intersection = left.intersection(right).count() as f32;
let baseline = left.len().max(right.len()) as f32;
intersection / baseline
}
/// Take pending memory if available and fresh for the given session.
pub fn take_pending_memory(session_id: &str) -> Option<PendingMemory> {
if let Ok(mut guard) = PENDING_MEMORY.lock() {
let map = guard.get_or_insert_with(HashMap::new);
if let Some(pending) = map.remove(session_id) {
if !pending.is_fresh() {
crate::memory_log::log_pending_discarded(session_id, "stale (>120s)");
return None;
}
// If every memory in this payload is still fresh in the session's
// injected set, the model already knows all of it; do not re-inject
// just because formatting or ranking shifted slightly.
if !pending.memory_ids.is_empty()
&& pending
.memory_ids
.iter()
.all(|id| is_memory_injected(session_id, id))
{
crate::memory_log::log_pending_discarded(
session_id,
"all memories already known to session",
);
return None;
}
let sig = prompt_signature(&pending.prompt);
if let Ok(mut last_guard) = LAST_INJECTED_PROMPT_SIGNATURE.lock() {
let sig_map = last_guard.get_or_insert_with(HashMap::new);
if let Some((last_sig, last_at)) = sig_map.get(session_id)
&& *last_sig == sig
&& last_at.elapsed().as_secs() < MEMORY_REPEAT_SUPPRESSION_SECS
{
crate::memory_log::log_pending_discarded(session_id, "duplicate suppressed");
return None;
}
sig_map.insert(session_id.to_string(), (sig, Instant::now()));
}
if !pending.memory_ids.is_empty() {
let pending_set = memory_set(&pending.memory_ids);
if let Ok(mut last_guard) = LAST_INJECTED_MEMORY_SET.lock() {
let set_map = last_guard.get_or_insert_with(HashMap::new);
if let Some((last_set, last_at)) = set_map.get(session_id) {
let overlap = memory_overlap_ratio(last_set, &pending_set);
if overlap >= MEMORY_SET_OVERLAP_SUPPRESSION_RATIO
&& last_at.elapsed().as_secs() < MEMORY_SET_REPEAT_SUPPRESSION_SECS
{
crate::memory_log::log_pending_discarded(
session_id,
"overlapping memory set suppressed",
);
return None;
}
}
set_map.insert(session_id.to_string(), (pending_set, Instant::now()));
}
}
if !pending.memory_ids.is_empty() {
mark_memories_injected(session_id, &pending.memory_ids);
}
crate::memory_log::log_pending_consumed(
session_id,
pending.count,
pending.computed_at.elapsed().as_millis() as u64,
pending.prompt.chars().count(),
);
return Some(pending);
}
}
None
}
/// Store a pending memory result for the given session.
pub fn set_pending_memory(session_id: &str, prompt: String, count: usize) {
set_pending_memory_with_ids(session_id, prompt, count, Vec::new());
}
/// Store a pending memory result with associated memory IDs for dedup tracking.
pub fn set_pending_memory_with_ids(
session_id: &str,
prompt: String,
count: usize,
memory_ids: Vec<String>,
) {
set_pending_memory_with_ids_and_display(session_id, prompt, count, memory_ids, None);
}
/// Store a pending memory result with associated memory IDs and optional display-only content.
pub fn set_pending_memory_with_ids_and_display(
session_id: &str,
prompt: String,
count: usize,
memory_ids: Vec<String>,
display_prompt: Option<String>,
) {
crate::memory_log::log_pending_prepared(session_id, &prompt, count, &memory_ids);
if let Ok(mut guard) = PENDING_MEMORY.lock() {
let map = guard.get_or_insert_with(HashMap::new);
let new_sig = prompt_signature(&prompt);
let new_memory_set = memory_set(&memory_ids);
if let Some(existing) = map.get(session_id)
&& existing.is_fresh()
{
let existing_sig = prompt_signature(&existing.prompt);
let overlap = memory_overlap_ratio(&memory_set(&existing.memory_ids), &new_memory_set);
if existing_sig == new_sig || overlap >= MEMORY_SET_OVERLAP_SUPPRESSION_RATIO {
crate::memory_log::log_pending_discarded(
session_id,
"similar pending payload already queued",
);
return;
}
}
map.insert(
session_id.to_string(),
PendingMemory {
prompt,
display_prompt,
computed_at: Instant::now(),
count,
memory_ids,
},
);
}
}
/// Mark memory IDs as already injected for a session (prevents re-injection on future turns).
pub fn mark_memories_injected(session_id: &str, ids: &[String]) {
crate::memory_log::log_marked_injected(session_id, ids);
insert_injected_ids(session_id, ids);
}
/// Mark memory IDs as already KNOWN to a session without them having been
/// injected, e.g. because they were just extracted from this session's own
/// transcript. The conversation already contains this information, so
/// re-injecting it would be a pure echo.
pub fn mark_memories_known(session_id: &str, ids: &[String], reason: &str) {
if ids.is_empty() {
return;
}
crate::memory_log::log_marked_known(session_id, ids, reason);
insert_injected_ids(session_id, ids);
}
fn insert_injected_ids(session_id: &str, ids: &[String]) {
if let Ok(mut guard) = INJECTED_MEMORY_IDS.lock() {
let outer = guard.get_or_insert_with(HashMap::new);
let set = outer
.entry(session_id.to_string())
.or_insert_with(HashMap::new);
set.retain(|_, at| injected_recently(at));
let now = Instant::now();
for id in ids {
set.insert(id.clone(), now);
}
crate::logging::info(&format!(
"[{}] Marked {} memory IDs as injected (total tracked: {})",
session_id,
ids.len(),
set.len()
));
}
}
/// Replace injected memory tracking for a session with the provided IDs.
/// Used when restoring persisted session state so the same logical session does
/// not re-inject memories after reload/resume.
pub fn sync_injected_memories(session_id: &str, ids: &[String]) {
if let Ok(mut guard) = INJECTED_MEMORY_IDS.lock() {
let outer = guard.get_or_insert_with(HashMap::new);
if ids.is_empty() {
outer.remove(session_id);
return;
}
let now = Instant::now();
outer.insert(
session_id.to_string(),
ids.iter().cloned().map(|id| (id, now)).collect(),
);
}
}
/// Check if a memory ID has already been injected for a session.
/// An injected ID "expires" after [`INJECTED_MEMORY_TTL_SECS`], at which point
/// the memory may be surfaced again.
pub fn is_memory_injected(session_id: &str, id: &str) -> bool {
if let Ok(guard) = INJECTED_MEMORY_IDS.lock()
&& let Some(outer) = guard.as_ref()
&& let Some(set) = outer.get(session_id)
&& let Some(at) = set.get(id)
{
return injected_recently(at);
}
false
}
/// Check if a memory ID has already been injected in ANY session.
/// Used by the singleton memory agent which doesn't track per-session state.
pub fn is_memory_injected_any(id: &str) -> bool {
if let Ok(guard) = INJECTED_MEMORY_IDS.lock()
&& let Some(outer) = guard.as_ref()
{
return outer
.values()
.any(|set| set.get(id).is_some_and(injected_recently));
}
false
}
/// Clear injected memory tracking for a session (call on session reset or topic change).
pub fn clear_injected_memories(session_id: &str) {
if let Ok(mut guard) = LAST_INJECTED_PROMPT_SIGNATURE.lock()
&& let Some(map) = guard.as_mut()
{
map.remove(session_id);
}
if let Ok(mut guard) = LAST_INJECTED_MEMORY_SET.lock()
&& let Some(map) = guard.as_mut()
{
map.remove(session_id);
}
if let Ok(mut guard) = INJECTED_MEMORY_IDS.lock()
&& let Some(outer) = guard.as_mut()
&& let Some(set) = outer.remove(session_id)
&& !set.is_empty()
{
crate::logging::info(&format!(
"[{}] Clearing {} tracked injected memory IDs",
session_id,
set.len()
));
}
}
/// Clear all injected memory tracking across all sessions.
pub fn clear_all_injected_memories() {
if let Ok(mut guard) = LAST_INJECTED_PROMPT_SIGNATURE.lock() {
*guard = None;
}
if let Ok(mut guard) = LAST_INJECTED_MEMORY_SET.lock() {
*guard = None;
}
if let Ok(mut guard) = INJECTED_MEMORY_IDS.lock() {
if let Some(outer) = guard.as_ref() {
let total: usize = outer.values().map(|s| s.len()).sum();
if total > 0 {
crate::logging::info(&format!(
"Clearing {} tracked injected memory IDs across {} sessions",
total,
outer.len()
));
}
}
*guard = None;
}
}
/// Clear any pending memory result for a session.
pub fn clear_pending_memory(session_id: &str) {
if let Ok(mut guard) = PENDING_MEMORY.lock()
&& let Some(map) = guard.as_mut()
{
map.remove(session_id);
}
if let Ok(mut guard) = LAST_INJECTED_PROMPT_SIGNATURE.lock()
&& let Some(map) = guard.as_mut()
{
map.remove(session_id);
}
if let Ok(mut guard) = LAST_INJECTED_MEMORY_SET.lock()
&& let Some(map) = guard.as_mut()
{
map.remove(session_id);
}
clear_injected_memories(session_id);
}
/// Clear all pending memory state across all sessions.
pub fn clear_all_pending_memory() {
if let Ok(mut guard) = PENDING_MEMORY.lock() {
*guard = None;
}
if let Ok(mut guard) = LAST_INJECTED_PROMPT_SIGNATURE.lock() {
*guard = None;
}
if let Ok(mut guard) = LAST_INJECTED_MEMORY_SET.lock() {
*guard = None;
}
clear_all_injected_memories();
}
/// Check if there's a pending memory for a specific session.
pub fn has_pending_memory(session_id: &str) -> bool {
PENDING_MEMORY
.lock()
.ok()
.and_then(|g| g.as_ref().map(|m| m.contains_key(session_id)))
.unwrap_or(false)
}
/// Check if there's any pending memory across all sessions.
pub fn has_any_pending_memory() -> bool {
PENDING_MEMORY
.lock()
.ok()
.and_then(|g| g.as_ref().map(|m| !m.is_empty()))
.unwrap_or(false)
}
pub(super) fn begin_memory_check(session_id: &str) -> bool {
if let Ok(mut guard) = MEMORY_CHECK_IN_PROGRESS.lock() {
let set = guard.get_or_insert_with(HashSet::new);
return set.insert(session_id.to_string());
}
false
}
pub(super) fn finish_memory_check(session_id: &str) {
if let Ok(mut guard) = MEMORY_CHECK_IN_PROGRESS.lock()
&& let Some(set) = guard.as_mut()
{
set.remove(session_id);
}
}
#[cfg(test)]
pub(super) fn insert_pending_memory_for_test(session_id: &str, pending: PendingMemory) {
let mut guard = PENDING_MEMORY.lock().expect("pending memory lock");
let map = guard.get_or_insert_with(HashMap::new);
map.insert(session_id.to_string(), pending);
}
#[cfg(test)]
pub(super) fn backdate_injected_memory_for_test(
session_id: &str,
id: &str,
age: std::time::Duration,
) {
if let Ok(mut guard) = INJECTED_MEMORY_IDS.lock()
&& let Some(outer) = guard.as_mut()
&& let Some(set) = outer.get_mut(session_id)
&& let Some(at) = set.get_mut(id)
{
*at = Instant::now() - age;
}
}