Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
230 lines
8.9 KiB
Rust
230 lines
8.9 KiB
Rust
//! `SessionMemory` — memory subsystem state for the session actor.
|
|
//!
|
|
//! Groups storage, flush config, injection state, and telemetry counters
|
|
//! that would otherwise be scattered across 15+ individual fields on
|
|
//! `SessionActor`.
|
|
|
|
use std::cell::RefCell;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64};
|
|
|
|
/// Memory subsystem state for a session.
|
|
pub struct SessionMemory {
|
|
/// Memory storage handle for writing flush output (None when memory disabled).
|
|
/// Wrapped in `RefCell` to allow `/memory on|off` toggle from `&Arc<SessionActor>`.
|
|
pub storage: RefCell<Option<crate::session::memory::MemoryStorage>>,
|
|
pub save_on_end: bool,
|
|
/// Shared params for building a fully-configured memory backend.
|
|
/// `None` when memory is disabled.
|
|
pub backend_params: Option<crate::session::memory::MemoryBackendParams>,
|
|
/// First-turn memory injection behavior resolved from local + remote config.
|
|
pub initial_injection_config: crate::config::MemoryInitialInjectionConfig,
|
|
/// Per-process latch: the first-turn injection decision already ran in
|
|
/// this session segment. Cross-segment idempotency comes from
|
|
/// `conversation_has_memory_context`, not this flag.
|
|
pub context_injected: AtomicBool,
|
|
pub flush_config: crate::config::MemoryFlushConfig,
|
|
/// When `true`, auto-compact checks are suppressed during memory flush.
|
|
pub is_flushing: AtomicBool,
|
|
/// The compaction count at which the last flush ran (once-per-cycle guard).
|
|
pub last_flush_compaction: AtomicU64,
|
|
pub flush_count: AtomicU64,
|
|
/// Content from the most recent successful flush, used for delta prompts.
|
|
/// Wrapped in `RefCell` because `SessionActor` is single-threaded (LocalSet).
|
|
pub last_flush_content: RefCell<Option<String>>,
|
|
pub flush_success_count: AtomicU64,
|
|
pub flush_error_count: AtomicU64,
|
|
/// Counts model-initiated `memory_search` tool calls.
|
|
/// Wrapped in `RefCell` to allow `/memory on|off` toggle from `&Arc<SessionActor>`.
|
|
pub search_counter: RefCell<Option<Arc<AtomicU64>>>,
|
|
/// Counts first-turn memory context injections.
|
|
pub injection_count: AtomicU64,
|
|
/// Counts post-compaction memory re-injection searches.
|
|
pub compaction_recovery_count: AtomicU64,
|
|
/// Total memory chunks added across all sources.
|
|
pub chunks_added: Arc<AtomicU64>,
|
|
/// autoDream consolidation config.
|
|
pub dream_config: crate::config::MemoryDreamConfig,
|
|
/// Number of dream consolidations attempted.
|
|
pub dream_count: AtomicU64,
|
|
pub dream_success_count: AtomicU64,
|
|
pub dream_error_count: AtomicU64,
|
|
}
|
|
|
|
impl SessionMemory {
|
|
pub fn is_enabled(&self) -> bool {
|
|
self.storage.borrow().is_some()
|
|
}
|
|
|
|
/// Clone the storage out of the `RefCell`, dropping the borrow immediately.
|
|
pub fn storage(&self) -> Option<crate::session::memory::MemoryStorage> {
|
|
self.storage.borrow().clone()
|
|
}
|
|
|
|
/// Attempt to acquire the flush lock. Returns `true` if acquired,
|
|
/// `false` if another flush is already in progress.
|
|
pub fn try_acquire_flush_lock(&self) -> bool {
|
|
self.is_flushing
|
|
.compare_exchange(
|
|
false,
|
|
true,
|
|
std::sync::atomic::Ordering::Relaxed,
|
|
std::sync::atomic::Ordering::Relaxed,
|
|
)
|
|
.is_ok()
|
|
}
|
|
|
|
pub fn release_flush_lock(&self) {
|
|
self.is_flushing
|
|
.store(false, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
|
|
/// Record a flush result and increment the appropriate counter.
|
|
///
|
|
/// Three-way outcome: "written" increments success, "error" increments
|
|
/// error, anything else ("nothing_to_store", "rejected") increments
|
|
/// only the total flush count.
|
|
pub fn record_flush_result(&self, outcome: &str) {
|
|
self.flush_count
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
match outcome {
|
|
"written" => {
|
|
self.flush_success_count
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
"error" => {
|
|
self.flush_error_count
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn record_dream_result(&self, success: bool) {
|
|
use std::sync::atomic::Ordering::Relaxed;
|
|
self.dream_count.fetch_add(1, Relaxed);
|
|
if success {
|
|
self.dream_success_count.fetch_add(1, Relaxed);
|
|
} else {
|
|
self.dream_error_count.fetch_add(1, Relaxed);
|
|
}
|
|
}
|
|
|
|
/// Record a neutral dream outcome (nothing to consolidate / skipped).
|
|
pub fn record_dream_neutral(&self) {
|
|
self.dream_count
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
|
|
/// Open (or create) the memory index for the current workspace.
|
|
///
|
|
/// Shared helper that extracts embed dimensions from `backend_params`
|
|
/// and opens the index at `<workspace_dir>/index.sqlite`.
|
|
pub(crate) fn open_index(
|
|
&self,
|
|
storage: &crate::session::memory::MemoryStorage,
|
|
) -> Option<crate::session::memory::MemoryIndex> {
|
|
let embed_dims = self
|
|
.backend_params
|
|
.as_ref()
|
|
.and_then(|p| p.embed_config.as_ref())
|
|
.map_or(1024, |c| c.dimensions);
|
|
let db_path = storage.workspace_dir().join("index.sqlite");
|
|
crate::session::memory::MemoryIndex::open_or_create(
|
|
&db_path,
|
|
storage.clone(),
|
|
Default::default(),
|
|
embed_dims,
|
|
)
|
|
.ok()
|
|
}
|
|
|
|
/// Reindex a file and embed new chunks when embedding is configured.
|
|
pub async fn reindex_and_embed(&self, path: &std::path::Path, source: &str) {
|
|
let Some(storage) = self.storage.borrow().clone() else {
|
|
return;
|
|
};
|
|
if let Some(mut index) = self.open_index(&storage) {
|
|
let _ = index.reindex_file(path, source);
|
|
if let Some(ref params) = self.backend_params
|
|
&& let Some(provider) = params.make_embedding_provider().await
|
|
{
|
|
crate::session::memory::embed_missing_chunks(&index, &provider).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Remove chunks for the given file paths from the search index.
|
|
///
|
|
/// Used after dream consolidation deletes processed session files so
|
|
/// that stale chunks don't linger in the index. Best-effort: errors
|
|
/// are logged but don't propagate.
|
|
pub fn delete_paths_from_index(&self, paths: &[std::path::PathBuf]) {
|
|
if paths.is_empty() {
|
|
return;
|
|
}
|
|
let Some(storage) = self.storage.borrow().clone() else {
|
|
return;
|
|
};
|
|
if let Some(mut index) = self.open_index(&storage) {
|
|
let mut total_removed = 0usize;
|
|
for path in paths {
|
|
match index.delete_path(path) {
|
|
Ok(n) => total_removed += n,
|
|
Err(e) => {
|
|
tracing::warn!(
|
|
target: kigi_log::memory_log::TARGET,
|
|
path = %path.display(),
|
|
error = %e,
|
|
"DREAM_CLEANUP: failed to remove chunks from index"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if total_removed > 0 {
|
|
tracing::info!(
|
|
target: kigi_log::memory_log::TARGET,
|
|
chunks_removed = total_removed,
|
|
files = paths.len(),
|
|
"DREAM_CLEANUP: removed stale chunks from index"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collect telemetry counters for session-end summary.
|
|
pub fn telemetry_snapshot(&self) -> MemoryTelemetry {
|
|
use std::sync::atomic::Ordering::Relaxed;
|
|
MemoryTelemetry {
|
|
flush_count: self.flush_count.load(Relaxed),
|
|
flush_success_count: self.flush_success_count.load(Relaxed),
|
|
flush_error_count: self.flush_error_count.load(Relaxed),
|
|
tool_search_count: self
|
|
.search_counter
|
|
.borrow()
|
|
.as_ref()
|
|
.map_or(0, |c| c.load(Relaxed)),
|
|
injection_count: self.injection_count.load(Relaxed),
|
|
compaction_recovery_count: self.compaction_recovery_count.load(Relaxed),
|
|
chunks_added: self.chunks_added.load(Relaxed),
|
|
dream_count: self.dream_count.load(Relaxed),
|
|
dream_success_count: self.dream_success_count.load(Relaxed),
|
|
dream_error_count: self.dream_error_count.load(Relaxed),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Snapshot of memory telemetry counters for session-end logging.
|
|
pub struct MemoryTelemetry {
|
|
pub flush_count: u64,
|
|
pub flush_success_count: u64,
|
|
pub flush_error_count: u64,
|
|
pub tool_search_count: u64,
|
|
pub injection_count: u64,
|
|
pub compaction_recovery_count: u64,
|
|
pub chunks_added: u64,
|
|
pub dream_count: u64,
|
|
pub dream_success_count: u64,
|
|
pub dream_error_count: u64,
|
|
}
|