Skip to content

fix(watch): degrade to lexical/graph-only mode on Intel Mac (no vector stack)#446

Merged
HumanBean17 merged 3 commits into
masterfrom
fix/watch-graph-only-intel-mac
Jul 13, 2026
Merged

fix(watch): degrade to lexical/graph-only mode on Intel Mac (no vector stack)#446
HumanBean17 merged 3 commits into
masterfrom
fix/watch-graph-only-intel-mac

Conversation

@HumanBean17

Copy link
Copy Markdown
Owner

Problem

A consumer on Intel Mac (darwin + x86_64) reports jrag watch crashes: it tries to import sentence_transformers, which PEP 508 markers in pyproject.toml exclude on that platform (sentence-transformers / lancedb / cocoindex are gated to sys_platform != 'darwin' or platform_machine != 'x86_64'). The daemon prints failed to load embedding model: No module named 'sentence_transformers' and exits 2 before serving.

jrag watch was the only code path that hard-required the vector stack. Every other path already degrades to lexical/graph-only search (mcp_v2.search_v2_ensure_vector_backend()ImportErrorrun_search=Nonerun_lexical_search; #443 made BM25/lexical first-class).

Root cause (two spots)

  1. Startupwatch/daemon.py eagerly called self.warm.model() to fail-fast on model-load errors. On Intel Mac that pulls sentence_transformersModuleNotFoundError → exit 2. (The daemon's read path doesn't even use warm.model()server.dispatch passes warm.graph() only; search_v2 loads the model lazily and falls back to lexical on its own. The eager call was purely fail-fast / first-query optimization.)
  2. Reindexwatch/watcher.py:reindex always ran run_cocoindex_update and treated its nonzero rc as fatal. On Intel Mac that returns a 127 stub (cocoindex binary absent — no raise), so every debounced reindex emitted a perpetual "vectors" error and never fired indexing_done.

Fix

Reuse the existing pipeline.vector_stack_installed() probe (cheap: 3× importlib.util.find_spec) to gate both spots:

  • watch/daemon.py — probe _vector_enabled in __init__; skip the model warm-up when vectors are absent (print a one-line lexical notice, continue serving). Add mode ("lexical" / "vector") to the state file and a panel row.
  • watch/watcher.py — probe _vector_enabled in __init__; in reindex, skip the cocoindex step (and its fatal check) when vectors are off, so the graph reindex still completes and fires indexing_done.
  • jrag.pywatch --status prints mode: lexical (graph-only) when applicable.

Outcome: the daemon now starts and stays useful on Intel Mac — serving warm lexical search (BM25 over the symbol graph) plus every structural command (find/inspect/callers/callees/flow), and reindexing the graph on file change.

Tests

  • tests/watch/test_daemon.py — graph-only startup: a subprocess runs the real run_foreground with daemon.vector_stack_installedFalse and a WarmResources.model() that raises if called (proving the gating); asserts the daemon reaches serving (not exit 2) and the state file carries mode: lexical.
  • tests/watch/test_watcher.py — graph-only reindex({"java"}) skips cocoindex, runs the graph under its COW snapshot lifecycle, and fires indexing_done; a sql change is a clean no-op.
  • Existing watcher tests pinned to _vector_enabled = True so the vector-path assertions are host-independent (don't implicitly depend on the host's vector stack).

Verification

  • Watch + watcher tests: 36 passed, 6 skipped (heavy).
  • Subset (watch/mcp/search/read_payloads): 413 passed, 26 skipped.
  • Full suite: 1538 passed, 38 skipped (~8 min).
  • Grep guard: no eager sentence_transformers/torch import anywhere in src/java_codebase_rag/watch/.
  • End-to-end graph-only simulation (throwaway, on Apple Silicon): built a graph-only index → spawned the daemon with the vector stack forced off → jrag search returned hot-served (cold-loads=0), mode=lexical, rc=0, with real symbol results — impossible on the vector path since no vectors were built, proving the lexical path ran over the socket.

Docs

docs/JAVA-CODEBASE-RAG-CLI.md, docs/CONFIGURATION.md, docs/ARCHITECTURE.md note the lexical/graph-only mode. Skills/agents watch tips stayed accurate (warm lexical is still warm).

Out of scope

  • No change to WarmResources.model() contract (only the daemon warm-up call is gated).
  • No new CLI flag — capability is environment-determined, matching the cold CLI.
  • pyproject.toml markers unchanged (Intel Mac stays graph-only by design).
  • No version bump (separate concern in this repo).

Related: #435 (lexical-mode direction), #443 (lexical first-class).

🤖 Generated with Claude Code

HumanBean17 and others added 3 commits July 13, 2026 23:27
…le (Intel Mac)

`jrag watch` was the only code path that hard-required the vector stack:
run_foreground eagerly called warm.model() (pulls sentence_transformers) and
watcher.reindex always ran cocoindex. On Intel Mac (darwin + x86_64) PEP 508
markers exclude sentence_transformers/lancedb/cocoindex, so the daemon printed
"failed to load embedding model" and exited 2 before serving; and a debounced
reindex would bail on a 127 cocoindex stub and never fire indexing_done.

Reuse pipeline.vector_stack_installed() (already used everywhere else) to gate:
- daemon: skip the model warm-up when vectors are absent; serve warm
  lexical/graph-only search (the read path, mcp_v2.search_v2, already degrades
  to lexical on its own). State/panel/--status carry mode=lexical.
- watcher: skip the cocoindex reindex step so the graph reindex completes and
  fires indexing_done instead of erroring forever.

Consistent with the cold CLI, which already runs lexical on Intel Mac (#443).

Tests: graph-only daemon startup (subprocess exercises real run_foreground;
WarmResources.model() raises if called, proving the gating) + graph-only
reindex (skips cocoindex, runs graph, fires indexing_done). Existing watcher
tests pinned to _vector_enabled=True so they're host-independent. Full suite
green (1538 passed, 38 skipped).

Co-Authored-By: Claude <noreply@anthropic.com>
Three-reviewer fan-out (core logic / tests / surface+docs) returned "ready to
merge" with no Critical/Important correctness issues. Applied the cheap,
high-value findings:

- skills/explore-codebase-cli/SKILL.md + agents/explorer-rag-cli.md: the shipped
  watch tip "(no per-call model load)" misrepresents the benefit on Intel Mac
  (no model exists there). Broadened to "(no per-call model/graph load; warm
  lexical + graph on Intel Mac)" — accurate for both install flavors. These are
  verbatim-shipped artifacts consumed by the exact audience this PR enables.
- tests/watch/test_daemon.py: the graph-only startup test captured no subprocess
  log and wasted the full 15s timeout on a regression that exits instantly. Now
  passes a log_path and polls proc.poll() for an early, diagnosable failure
  (_fail_with_log surfaces the log tail).
- tests/watch/test_watcher.py: graph-only java reindex upgraded from membership
  asserts to the exact calls-list sequence (matches the sibling vector-path
  test's style; catches a spurious extra call).
- jrag.py: `watch --status` mode line now prints just "lexical (graph-only)",
  matching the TTY panel and the CLI doc (was "— no vector ranking").
- watch/daemon.py: one-line comment clarifying _state["mode"] derives from the
  install-time probe, not a live search-capability check (the read path's actual
  lexical/vector choice is mcp_v2._ensure_vector_backend; they agree under the
  PEP 508 markers).
- watch/watcher.py: wrapped the pipeline import to fit the 100-col line limit.
- docs/JAVA-CODEBASE-RAG-CLI.md: hedged the watch intro's "skips the per-call
  torch/model load" to "model/graph load" + noted graph-only on Intel Mac.

Deferred (pre-existing, out of scope): no positive-direction test that the
vector-ENABLED daemon warms model() — the vector path was verified byte-unchanged
vs base by review; adding it would require pinning _vector_enabled in the shared
stub + a cross-process sentinel.

Watch tests: 36 passed, 6 skipped. No new ruff errors (the 4 pre-existing
jrag.py F-codes exist on origin/master).

Co-Authored-By: Claude <noreply@anthropic.com>
CI (test_install_data_artifacts_in_sync_with_dev_source) failed because the
review-fix commit edited the dev-source copies in skills/ and agents/ but not
the bundled copies under src/java_codebase_rag/install_data/, which the sync
check requires to be byte-equal. Regenerated via `scripts/sync_agent_artifacts.py`
for the two edited watch tips (explorer-rag-cli.md + explore-codebase-cli/SKILL.md).

Co-Authored-By: Claude <noreply@anthropic.com>
@HumanBean17 HumanBean17 merged commit ee2eee0 into master Jul 13, 2026
4 checks passed
@HumanBean17 HumanBean17 deleted the fix/watch-graph-only-intel-mac branch July 13, 2026 21:07
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