Skip to content

fix(server): don't disable embeddings when macOS misreports 0 MB free#14

Merged
anvanster merged 4 commits into
mainfrom
fix/macos-memory-detection
Jul 19, 2026
Merged

fix(server): don't disable embeddings when macOS misreports 0 MB free#14
anvanster merged 4 commits into
mainfrom
fix/macos-memory-detection

Conversation

@anvanster

Copy link
Copy Markdown
Member

Intent

The developer asked the agent to investigate a user-reported GitHub issue (#13) on the CodeGraph repo where the server disabled embeddings/model loading because macOS misreported 0 MB of available memory via sysinfo's available_memory(). They wanted the gate fixed so a 0 MB reading is treated as a detection failure that proceeds with the model load, plus a CODEGRAPH_SKIP_MEMORY_CHECK environment-variable escape hatch, with unit tests covering the load/skip/zero/bypass/boundary cases. They explicitly asked to document CODEGRAPH_SKIP_MEMORY_CHECK in the README. Finally, they instructed the agent to post the drafted resolution comment on issue #13 and push the fix/macos-memory-detection branch (through the no-mistakes validation gate rather than directly to origin), noting the fix would need to be folded into the latest code currently in testing. This work sat alongside broader session efforts (VS Code extension funnel/CodeLens features, vsix packaging, and userbase-growth planning) but the commit itself was the isolated macOS memory-detection fix off main.

What Changed

  • Extracted the pre-model-load RAM gate into a pure, unit-tested evaluate_memory_gate policy in crates/codegraph-server/src/memory.rs: a 0-byte available_memory() reading is now treated as a detection failure (common on macOS, issue macOS: available memory misdetected as 0 MB — embedding model never loads, markdown/memory tools unusable #13) and the ONNX embedding model loads anyway, instead of hard-disabling semantic search; genuinely low readings still skip the model with a graph-only fallback.
  • Added a CODEGRAPH_SKIP_MEMORY_CHECK env-var escape hatch to bypass the gate entirely, referenced in the skip-path log/error messages and documented in both the main README and the npm mcp-package README.
  • Applied the same shared gate to the socket-engine shared-model path (mcp/engine.rs) and taught the embed-loop RAM backpressure (ai_query/engine.rs) to ignore 0 MB readings, so misreporting machines no longer fall back to per-workspace model loads or permanently ratchet batch size down to the minimum. Covered by new gate/backpressure unit tests plus e2e runs against a fake vm_stats shim exercising the zero, low, and bypass paths (all passing in the pipeline).

Risk Assessment

✅ Low: Well-bounded fix with the gate policy extracted into a pure, unit-tested helper, both previously flagged inconsistent call sites (socket-engine gate and embed backpressure) now correctly reusing it, no remaining raw memory checks, and README documentation matching actual behavior.

Testing

Ran all 8 new unit tests (gate policy load/skip/zero/bypass/boundary plus embed-backpressure zero-reading cases; all pass), then exercised the fix end-to-end by running the real codegraph-server binary with a DYLD interpose shim that reproduces sysinfo's macOS 0 MB misreport: the 0 MB case now proceeds to load the embedding model (one-shot and --serve engine paths), genuinely low memory still skips with the new override hint, and CODEGRAPH_SKIP_MEMORY_CHECK=1 bypasses the gate live; log transcripts and the shim source are saved as evidence. No UI surface is involved, so log transcripts are the reviewer-visible artifact.

Evidence: Memory-gate e2e log transcript (baseline / 0 MB repro / low-skip / bypass / socket engine)
# macOS memory-gate fix (issue #13) - end-to-end verification

Real 'codegraph-server' debug binary run on macOS. The 0 MB / low-memory
readings are injected by a DYLD interpose shim on host_statistics64 that
reproduces the exact sysinfo failure mode (compressor pages exceed
free+inactive+purgeable, so the saturating subtraction yields 0).

## 1. Baseline (no shim): healthy Mac, model loads
`` `
2026-07-19T04:44:01.171550Z  INFO codegraph_server::memory: [MemoryManager::initialize] available memory: 6951 MB
2026-07-19T04:44:01.171734Z  INFO codegraph_memory::embedding::fastembed_embed: Loading embedding model: BGE-Small-EN-v1.5 (384d, 512-tok context)
2026-07-19T04:44:01.317533Z  INFO codegraph_server::memory: [MemoryManager::initialize] Memory initialized at "/Users/anvanster/.codegraph/projects/cg-memgate-ws-qcwm-4997/memory"
`` `

## 2. Issue #13 repro: sysinfo reports 0 MB -> detection failure, model STILL loads (the fix)
`` `
2026-07-19T04:45:17.290998Z  INFO codegraph_server::memory: [MemoryManager::initialize] available memory: 0 MB
2026-07-19T04:45:17.291005Z  WARN codegraph_server::memory: [MemoryManager::initialize] available memory read as 0 MB — treating as a detection failure (common on macOS, where reclaimable memory is not counted as free) and proceeding with the embedding model load. Set CODEGRAPH_SKIP_MEMORY_CHECK=1 to always bypass this check.
2026-07-19T04:45:17.291065Z  INFO codegraph_memory::embedding::fastembed_embed: Loading embedding model: BGE-Small-EN-v1.5 (384d, 512-tok context)
2026-07-19T04:45:17.409268Z  INFO codegraph_server::memory: [MemoryManager::initialize] Memory initialized at "/Users/anvanster/.codegraph/projects/cg-memgate-ws-qcwm-4997/memory"
`` `

## 3. Genuinely low memory (499 MB) -> model skipped, graph-only, override hint shown
`` `
2026-07-19T04:45:24.374805Z  INFO codegraph_server::memory: [MemoryManager::initialize] available memory: 499 MB
2026-07-19T04:45:24.374853Z  WARN codegraph_server::memory: [MemoryManager::initialize] only 499 MB available — skipping embedding model to avoid OOM; semantic search disabled (graph-only). Set CODEGRAPH_SKIP_MEMORY_CHECK=1 to override.
2026-07-19T04:45:24.374866Z  WARN codegraph_server::mcp::server: Failed to initialize memory manager: Other("insufficient memory (499 MB available) to load embedding model; running graph-only (set CODEGRAPH_SKIP_MEMORY_CHECK=1 to override)")
`` `

## 4. Low memory + CODEGRAPH_SKIP_MEMORY_CHECK=1 -> bypass, model loads anyway
`` `
2026-07-19T04:45:30.272221Z  INFO codegraph_server::memory: [MemoryManager::initialize] available memory: 499 MB
2026-07-19T04:45:30.272282Z  INFO codegraph_memory::embedding::fastembed_embed: Loading embedding model: BGE-Small-EN-v1.5 (384d, 512-tok context)
2026-07-19T04:45:30.390570Z  INFO codegraph_server::memory: [MemoryManager::initialize] Memory initialized at "/Users/anvanster/.codegraph/projects/cg-memgate-ws-qcwm-4997/memory"
`` `

## 5. Resident socket engine (--serve) with 0 MB reading -> shared model still loads
`` `
2026-07-19T04:45:42.638473Z  WARN codegraph_server::mcp::engine::imp: Engine: available memory read as 0 MB — treating as a detection failure (common on macOS, where reclaimable memory is not counted as free) and proceeding with the shared model load. Set CODEGRAPH_SKIP_MEMORY_CHECK=1 to always bypass this check.
2026-07-19T04:45:42.787735Z  INFO codegraph_server::mcp::engine::imp: Engine: shared embedding model loaded
2026-07-19T04:45:46.792230Z  INFO codegraph_server::mcp::engine::imp: Engine: idle 3s with no clients — shutting down
`` `
Evidence: DYLD interpose shim used to reproduce the macOS 0 MB sysinfo misreport

Interposes host_statistics64 and inflates compressor_page_count so sysinfo's (free+inactive+purgeable - compressor) saturating subtraction yields 0 — the exact issue-13 failure mode; FAKE_VM_MODE=low fakes ~500 MB for the skip/bypass paths.

#include <mach/mach.h>
#include <stdlib.h>
#include <unistd.h>

// Interpose host_statistics64 to simulate the macOS condition from
// CodeGraph issue #13: the compressor holds more pages than
// free+inactive+purgeable, so sysinfo's saturating subtraction yields
// 0 bytes "available" on a machine that actually has plenty of RAM.
static kern_return_t my_host_statistics64(host_t host, host_flavor_t flavor,
                                          host_info64_t info,
                                          mach_msg_type_number_t *count) {
    kern_return_t r = host_statistics64(host, flavor, info, count);
    if (flavor == HOST_VM_INFO64 && r == KERN_SUCCESS) {
        vm_statistics64_data_t *vm = (vm_statistics64_data_t *)info;
        const char *mode = getenv("FAKE_VM_MODE");
        if (mode && mode[0] == 'z') {
            // free+inactive+purgeable - compressor saturates to 0
            vm->compressor_page_count =
                vm->free_count + vm->inactive_count + vm->purgeable_count + 1000;
        } else if (mode && mode[0] == 'l') {
            // genuinely low: ~500 MB available
            long pgsz = sysconf(_SC_PAGESIZE);
            vm->free_count = (natural_t)(500000000ULL / (unsigned long long)pgsz);
            vm->inactive_count = 0;
            vm->purgeable_count = 0;
            vm->compressor_page_count = 0;
        }
    }
    return r;
}

__attribute__((used)) static struct {
    const void *repl;
    const void *orig;
} interposers[] __attribute__((section("__DATA,__interpose"))) = {
    {(const void *)my_host_statistics64, (const void *)host_statistics64},
};

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ crates/codegraph-server/src/mcp/engine.rs:222 - The socket-engine shared-model gate still uses the raw sys.available_memory() &lt; 1_500_000_000 check with no zero-reading (detection-failure) handling and no CODEGRAPH_SKIP_MEMORY_CHECK support, despite its comment saying it gates 'the same way the per-workspace path does'. On a macOS machine misreporting 0 MB (issue macOS: available memory misdetected as 0 MB — embedding model never loads, markdown/memory tools unusable #13), the shared model is skipped and every workspace falls back to loading its own ONNX model via the now-fixed per-workspace gate - multiplying memory usage on exactly the machines flagged as constrained, and making the README claim that the env var works in MCP mode only accidentally true. Fix by reusing evaluate_memory_gate, memory_check_bypassed, and MODEL_MIN_FREE_BYTES here (they are already pub(crate)).
  • ⚠️ crates/codegraph-server/src/ai_query/engine.rs:401 - The embed-loop RAM backpressure (also at line 550) treats a 0 MB available_memory reading as genuine pressure: on the same misreporting macOS machines, every poll of available_memory_mb() &lt; EMBED_LOW_MEM_MB is true, so the ONNX batch size ratchets down to the minimum of 4 and a checkpoint is forced on every poll, permanently degrading embedding throughput during first index. Apply the same '0 is an untrusted reading, not real state' rule introduced by this commit (e.g. avail_mb &gt; 0 &amp;&amp; avail_mb &lt; EMBED_LOW_MEM_MB).

🔧 Fix: apply shared memory gate to socket engine and embed backpressure
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • cargo test -p codegraph-server --lib gate_ (5 MemoryGate policy tests: ample/boundary/low-skip/zero/bypass — all pass)
  • cargo test -p codegraph-server --lib embed_memory_pressured (3 embed-backpressure tests incl. zero-reading-not-pressure — all pass)
  • cargo test -p codegraph-server --lib memory:: (full memory module suite — 26 pass, 1 ignored needing model files)
  • Baseline e2e: RUST_LOG=info ./target/debug/codegraph-server --workspace <tmp> --run-tool codegraph_get_symbol_info — 6951 MB read, model loads normally
  • Issue #13 e2e repro: same command under DYLD_INSERT_LIBRARIES=fake_vm_stats.dylib FAKE_VM_MODE=zero — sysinfo reads 0 MB, server logs detection-failure warning and still loads the embedding model
  • Skip-path e2e: FAKE_VM_MODE=low (499 MB) — model skipped, graph-only, log and error now carry the CODEGRAPH_SKIP_MEMORY_CHECK=1 override hint
  • Bypass e2e: FAKE_VM_MODE=low CODEGRAPH_SKIP_MEMORY_CHECK=1 — model loads despite 499 MB reading
  • Socket-engine e2e: ./target/debug/codegraph-server --serve under FAKE_VM_MODE=zero — 'Engine:' detection-failure warning logged and shared embedding model loaded
  • Confirmed README diff documents CODEGRAPH_SKIP_MEMORY_CHECK and worktree left clean after testing
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

anvanster and others added 4 commits July 18, 2026 21:21
…lable (#13)

MemoryManager::initialize gates the ONNX model load on available memory to
avoid OOM-killing the process. sysinfo's available_memory() returns 0 on some
macOS versions (reclaimable memory is parked in inactive/speculative/purgeable
pages that aren't counted as free), so healthy Macs with 16 GB RAM had every
embedding-dependent tool (index_markdown, memory_*, semantic search) silently
disabled with no override.

- Extract evaluate_memory_gate() as a pure, unit-tested policy: a 0 reading is
  a detection failure (a running process always holds memory) -> proceed with
  the load rather than hard-disabling on a number we don't trust; a genuine
  low-but-nonzero reading still skips to graph-only.
- Add CODEGRAPH_SKIP_MEMORY_CHECK=1 (also true/yes) to bypass the gate entirely,
  in both MCP and one-shot --run-tool modes.
- Log/error messages now name the override; README documents it.
- 5 unit tests cover load / skip / zero-proceed / bypass / boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 CodeGraph PR Review

5 files changed (+202/−23, 18 functions) · Risk: 🔴 high

Blast radius

48 direct callers affected across codegraph-server/src/ai_query, codegraph-server/src/mcp, crates/codegraph-server/src

⚠️ Test gaps (16 functions, 0 coverage)

  • split_identifier_words_handles_camel_and_snake (crates/codegraph-server/src/ai_query/engine.rs) — body_changed
  • embed_memory_pressured_low_reading_is_pressure (crates/codegraph-server/src/ai_query/engine.rs) — signature_changed
  • build_embed_text_prepends_split_name_only_when_enabled (crates/codegraph-server/src/ai_query/engine.rs) — body_changed
  • embed_memory_pressured (crates/codegraph-server/src/ai_query/engine.rs) — signature_changed
  • embed_memory_pressured_zero_reading_is_detection_failure_not_pressure (crates/codegraph-server/src/ai_query/engine.rs) — signature_changed
  • embed_memory_pressured_at_or_above_floor_is_not_pressure (crates/codegraph-server/src/ai_query/engine.rs) — signature_changed
  • build_symbol_vectors_checkpointed (crates/codegraph-server/src/ai_query/engine.rs) — body_changed
  • embed_missing_symbols_checkpointed (crates/codegraph-server/src/ai_query/engine.rs) — body_changed
  • serve (crates/codegraph-server/src/mcp/engine.rs) — body_changed
  • gate_bypass_forces_load_regardless_of_reading (crates/codegraph-server/src/memory.rs) — signature_changed
  • …and 6 more

Suggested reviewers

Andrey Vasilevsky (53 lines)

Suggested commit: feat(ai_query): <describe the change> · 5 tests cover the changes
🤖 Generated by CodeGraph

@anvanster
anvanster merged commit 06bcc88 into main Jul 19, 2026
1 check passed
@anvanster
anvanster deleted the fix/macos-memory-detection branch July 19, 2026 05:05
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