perf(ooda): bulk graph-adjacency index — prepare-context from ~11 min to seconds (#40) - #3046
Merged
Merged
Conversation
…min to seconds (#40) The OODA daemon's per-cycle prepare-context step (`preparation_memory_operations_with_active_slugs_phased`) spun for ~11 min at ~300% CPU every cycle with no child processes and no log output — the substance behind the operator's "the memory graph never loads" complaint. The graph loads; the read was pathologically slow, so each cycle took ~15–18 min. Root cause was engine-side: ranked declarative/episodic recall computed the graph-proximity term with a per-node `query_neighbors` BFS — an N+1 fan-out of ~3N lock-serialized Cypher scans per recall (~23k queries at ~7,590 facts). Fixed in amplihack-memory-lib PR #126 (bulk graph-adjacency index: one bulk edge scan per type, byte-identical ranking). This PR consumes that fix: - Bump the pinned `amplihack-memory` git rev to the PR #126 squash-merge commit (72c5ea1) and rebuild `Cargo.lock`. Pure performance refactor upstream; no store-format change (still v41), same relevant facts/triggers/procedures/ episodes surface with recency/phase weighting intact. - Add per-sub-step `tracing` spans (counts/timings only — never memory content) around the four prepare-context sub-ops (ranked declarative recall, check_triggers, procedure recall, ranked episodic recall) plus the whole call, so per-sub-step wall-clock is attributable from a live log without a profiler. The engine emits `graph_path` (indexed|legacy); Simard emits the timings. - Preserve the operator-facing "Prepared context: N facts, …" working-memory summary verbatim. Regression guard: `memory_consolidation::tests_prepare_context_perf` pins the orchestration invariant that prepare-context issues a small, CONSTANT number of expensive recall calls — O(objective fragments), never O(fact-store size) — via deterministic call-count assertions (no wall-clock timeout), and that the "Prepared context: …" summary keeps reaching working memory. This fails the moment a per-fact fan-out is reintroduced at the orchestration layer, one layer above the engine fix. Engine profiling (persistent/lbug, 1,500 facts, 1 hop): legacy 3,000 per-node Cypher scans / 14,688 ms -> indexed 0 per-node scans, 2 bulk scans / 286 ms (51.3x); round-trips now constant in fact count. Docs: add the bulk graph-adjacency index reference and mark the ranked-recall graph signal as shipped. Closes #40 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rysweet
force-pushed
the
feat/issue-40-bulk-graph-adjacency
branch
from
July 8, 2026 03:20
1cfdd68 to
8826efb
Compare
📊 Coverage Summary
Coverage data from CI run. Test files matching |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The OODA daemon's per-cycle prepare-context step
(
preparation_memory_operations_with_active_slugs_phased,src/memory_consolidation/mod.rs)spun for ~11 min at ~300% CPU every cycle with no child processes and no log
output — the substance behind the operator's "the memory graph never loads"
complaint. The graph/facts do load; the read was pathologically slow, so each
OODA cycle took ~15–18 min and the dashboard/memory views felt stuck. (#40)
Confirmed not a regression: the prior deploy showed the identical ~10.5-min
gap. It was a stable, pre-existing performance pathology.
Root cause (profiled, engine-side)
The cognitive memory holds ~7,590 facts; the result is tiny (10 facts, 5
procedures, 5 episodes), so the cost was in scanning/ranking, not output size.
Ranked declarative/episodic recall computed the graph-proximity score by
running a bounded BFS from every candidate node — one
query_neighborspernode per traversal edge type per hop, an N+1 fan-out of ~3N lock-serialized
Cypher scans per recall (~23k queries at ~7,590 facts). Per binding policy the
engine fix lives in the memory library.
What this PR does (Simard side)
The substantive fix shipped in
amplihack-memory-lib PR #126
(bulk graph-adjacency index: one bulk edge scan per type, byte-identical
ranking). This PR consumes it:
amplihack-memorygit rev901f63a → 72c5ea1(the PR fix: remaining review findings — path traversal, dead field, env caching #126squash-merge commit) + rebuilt
Cargo.lock. Pure performance refactorupstream; no store-format change (still v41). The same relevant
facts/triggers/procedures/episodes surface with recency/phase weighting intact.
tracingspans (counts/timings only — nevermemory content, log-hygiene SR-9) around the four expensive prepare-context
sub-ops (ranked declarative recall,
check_triggers, procedure recall, rankedepisodic recall) plus the whole call, so per-sub-step wall-clock is
attributable from a live log without a profiler. The engine emits
graph_path(
indexed|legacy); Simard emits the timings. The operator-facing"Prepared context: N facts, …"working-memory summary is preserved verbatim.cognitive-memory-graph-adjacency-index.mdreference + mark theranked-recall graph signal as shipped.
Regression guard
memory_consolidation::tests_prepare_context_perf(new) pins the orchestrationinvariant that prepare-context issues a small, CONSTANT number of expensive
recall calls — O(objective fragments), never O(fact-store size) — via
deterministic call-count assertions (no wall-clock timeout, per policy), and
that the
"Prepared context: …"summary keeps reaching working memory. It failsthe moment a per-fact fan-out is reintroduced at the orchestration layer, one
layer above the engine fix. A counting
CognitiveMemoryOpsmock varies resultvolume (up to 5,000 facts) independently of call count to prove volume never
multiplies calls.
Profiling evidence (before / after)
Engine harness on the real persistent (LadybugDB) backend, 1,500 facts, 1 hop:
query_neighbors51.3× faster; store round-trips 3,000 → 2 and now constant in fact count.
Each legacy call is a lock-serialized Cypher scan — the cost that reaches ~11 min
at ~7,590 facts across multi-hop BFS and per-fragment recalls. Prepare-context
now completes in well under 30 s (single-digit seconds), restoring OODA cadence.
Validation
Local:
cargo fmt --check,cargo clippy --all-targets --all-features -D warnings,cargo test --release --lib (cognitive_memory|…|memory_consolidation)all green (pre-push gate). No wall-clock timeouts added; no fallbacks/silent
degradation.
Depends on: rysweet/amplihack-memory-lib#126 (merged).
Closes #40