Skip to content

feat(memory): event-sourced associative recall + importance tool + arch cleanup#188

Merged
yishuiliunian merged 3 commits into
mainfrom
worktree-reactive-rolling-fairy
May 31, 2026
Merged

feat(memory): event-sourced associative recall + importance tool + arch cleanup#188
yishuiliunian merged 3 commits into
mainfrom
worktree-reactive-rolling-fairy

Conversation

@yishuiliunian
Copy link
Copy Markdown
Contributor

Summary

  • Associative recall: per-session JSONL event log persists RecallStats (frequency + importance) across sessions. Verified per-Q warmup lift of MRR +7.4% / R@10 +16.1% on the 58-query eval fixture.
  • Production importance tool: MemoryImportanceTool lets LLM/user mark salient nodes. Previously emotional_bonus read a field that had no writer — the path was dead.
  • Architecture cleanup: EmotionalTag → ImportanceTag rename (no emotion model, only i8 importance), event_log/ extracted from store/, MemorySubsystem::bootstrap owns its own lifecycle.

Changes

Core additions

  • loopal-memory/src/event_log/: writer / fold / gc / schema / recall_stats — per-session append-only JSONL, git-mergeable, transparent .gz read
  • graph::recall::emit_events: writes QueryEvent + RecallHit per call
  • graph::score::recall_reinforcement_bonus + importance_bonus: ranking signal from accumulated stats
  • tools::importance::MemoryImportanceTool: production write path (1..=10 salience scale)
  • MemorySubsystem::bootstrap(memory_dir, session_dir, events_dir, sid, gc_compress_days, gc_archive_days): lifecycle moved out of agent-server::build_subsystem
  • MemoryConfig: gc_compress_after_days (90), gc_archive_after_days (365)
  • init::ensure_gitignore: auto-managed gitignore rules for memory-events/

Bug fixes (8-angle workflow sweep, 13 confirmed → 6 unique)

  • compress_file orphan recovery: .jsonl + .jsonl.gz coexistence after crash → fold double-count (2× recall_count inflation)
  • compress_file concurrent GC race: per-process .tmp.<pid>.<nanos> suffix replaces fixed .tmp
  • fold_one_file partial-batch recovery: single IO error no longer drops already-parsed events
  • run_gc read_dir error: now counts stats.errors instead of silent zero-return
  • compress_file: io::copy stream replaces read_to_end (was OOM on multi-GB files)
  • ensure_gitignore: from_utf8_lossy handles non-UTF8 existing files (was appending indefinitely)

Architecture cleanup (arch_check)

  • EmotionalTag → ImportanceTag rename (13 symbols + serde wire tag)
  • install_recall_stats(&mut self) (was &self, allowed post-Arc race)
  • gitignore moved out of store/ (storage shouldn't reach project root)
  • MemoryGraph::record_event made pub + delete record_emotional_tag wrapper
  • recall.rs (311 lines) → recall/{mod,anchors,emit}.rs
  • queries_node.rs (222 lines) → queries_node/{mod,read,write}.rs
  • Dead code removed: CoRecall emit (no fold), NearMiss schema (never emitted)

Eval framework

  • 47 new fixture .md across 4 domains (postgres ops / ci-cd / auth-security / observability)
  • 48 new ground-truth queries (10 → 58 total)
  • A/B mode: cold vs warm 5x/20x vs Imp +5/+10 vs Per-Q 5x
  • Per-Q lift: MRR 0.753 → 0.809, R@10 0.740 → 0.859

Tests

  • 207 memory crate tests passing (+ 22 new test files in this PR)
  • 8 GC recovery / 6 importance tool / 2 associative recall integration / 6 prior associated tests

Test plan

  • CI green (clippy + rustfmt + bazel test //...)
  • 200-line file limit: 0 violations
  • Eval gates run successfully (cold gates pre-existing FAIL on expanded fixture — not a regression)

…ch cleanup

Why: memory_recall previously had no reinforcement signal (cold-start every
session) and no way for the LLM to mark salient nodes — limiting it to pure
graph traversal. This change makes recall behavior accumulate across sessions
via an append-only event log, and exposes user/LLM intent through a new
importance tool. Verified per-query lift of MRR +7.4% / R@10 +16.1% on the
expanded 58-query eval fixture.

Core additions
- event_log/: per-session JSONL append-only log (writer / fold / gc / schema /
  recall_stats), git-mergeable by design; folds into in-memory RecallStats on
  session start
- graph::recall now emits QueryEvent + RecallHit; scoring layer adds
  recall_reinforcement_bonus (frequency) and importance_bonus (user intent)
- MemoryImportanceTool: production write path so LLM/user can mark nodes
  important; previously the score path read a field that had no writer
- MemorySubsystem::bootstrap: memory crate owns its own orchestration
  (canonicalize → gc → fold → install_stats → set_event_log → scan → watch)
  instead of agent-server stitching primitives
- MemoryConfig: gc_compress_after_days (90) + gc_archive_after_days (365)
  replace hardcoded constants
- ensure_gitignore: auto-managed gitignore rules for memory-events/

Bug fixes found via multi-angle sweep (8 angles × verify)
- compress_file orphan recovery (.jsonl + .jsonl.gz coexistence after crash
  between rename and remove caused 2× recall_count inflation)
- compress_file concurrent GC race (per-process pid+nanos tmp suffix
  replaces fixed .tmp that two sessions could truncate)
- fold_one_file partial-batch recovery (single IO error no longer drops the
  entire file's already-parsed events)
- run_gc read_dir error now counts toward stats.errors instead of silent
  zero-return
- compress_file streams via io::copy instead of read_to_end (was OOM-prone
  on multi-GB session files)
- ensure_gitignore handles non-UTF8 existing files via lossy decode (was
  appending rules indefinitely on every session start)

Architecture cleanup (arch_check)
- EmotionalTag → ImportanceTag rename (13 symbols + serde wire tag) — the
  field was always literally i8 importance with no emotion model
- install_recall_stats now &mut self (compile-time prevents post-Arc race)
- gitignore moved out of store/ (store layer should not reach project root)
- MemoryGraph::record_event made pub (delete record_emotional_tag wrapper —
  inconsistent with recall.rs use-site construction pattern)
- store/event_log_* + recall_stats moved to event_log/ submodule (separate
  concerns: SQLite vs event sourcing)
- recall.rs (311 lines) split into recall/{mod,anchors,emit}.rs
- queries_node.rs (222 lines) split into queries_node/{mod,read,write}.rs
- Dead code removed: CoRecall emit (no fold consumer), NearMiss schema
  (never emitted), runner::run() (replaced by run_with_warmup(0))

Testing
- 47 new fixture .md files + 48 new ground-truth queries (4 domain clusters:
  postgres ops, ci/cd, auth/security, observability)
- per-query warmup A/B framework in eval (cold vs warm 5x/20x vs Imp +5/+10
  vs Per-Q 5x)
- 8 GC recovery tests (orphan / concurrent tmp / streaming / read_dir error
  / archive collision / fold partial recovery / non-UTF8 gitignore)
- 6 importance tool tests (metadata / validation / e2e write+fold / repeated
  tagging / silent drop without event log)
- 2 associative recall integration tests (reinforcement persists across
  sessions; importance lifts ranking)

Verification
- bazel test //... — all passing (207 memory crate + 6 other targets)
- bazel build //... --config=clippy --config=rustfmt — clean
- 200-line file limit — 0 violations
- eval gates: cold R@5=0.579, MRR=0.753; per-Q lift +7.4% MRR, +16.1% R@10
Artifact from initial fixture-generation script where an empty slug produced
a 0-byte file named literally ".md" in the fixtures directory. Not referenced
by fixture.rs registry; harmless but noise.
gc_propagates_read_dir_permission_error uses std::os::unix::fs::PermissionsExt
to chmod 000 a directory and verify run_gc surfaces the read_dir error. The
unix API is gated out on Windows, breaking the Windows CI build. Skip the
test on non-unix targets; the production code path (run_gc → fs::read_dir →
io::ErrorKind::PermissionDenied → stats.errors += 1) is platform-agnostic
and covered by unix runs.
@yishuiliunian yishuiliunian merged commit e424368 into main May 31, 2026
4 checks passed
@yishuiliunian yishuiliunian deleted the worktree-reactive-rolling-fairy branch May 31, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant