AI second brain Phase 3: graph-aware retrieval live in the assistant (0211) - #232
Merged
Conversation
…se 3)
The assistant's context pack now uses @xnetjs/brain's retrieve() via the
AiSurfaceService retrieveContext seam, instead of a flat keyword scan:
- New ai-graph-retriever.ts: createGraphContextRetriever builds an
AiContextRetriever over the local NodeStore — keyword entry search + bounded
expansion along typed relations (resolved from the schema registry) + token
budget + readable provenance paths. Deliberately model-free (no embedding
download) so cold-start is untouched per 0204; the vector tier can swap in
behind the same seam later.
- AiChatPanel injects it into createAiSurfaceService({ retrieveContext }).
- Export AiContextRetriever/AiRetrievedNode from the @xnetjs/plugins barrels.
- 6 new tests; verified in-browser (entry + 1-hop neighbor with path label).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
🖼️ UI changes in this PRNo visual differences detected in the changed UI. |
Contributor
|
Preview removed for PR #232. |
crs48
added a commit
that referenced
this pull request
Jun 22, 2026
Follow-up to exploration [0211](docs/explorations/0211_[_]_AI_SECOND_BRAIN_GRAPHRAG_MEMORY_AND_TIERING.md) (after [#228](#228) / [#230](#230) / [#232](#232)) — ships the **just-in-time expansion** half of the anti-overwhelm design. ## What ships A built-in `AiSurfaceService` tool, **`xnet_graph_expand`**, that walks typed relation edges out from a node to its connected neighbors, bounded by `hops` (1–2) and a result `limit`. - Resolves relation fields from the schema registry; returns each neighbor with the **relation + direction** it came through (traceable answers). - **Auto-surfaced to MCP/CLI agents** via `getTools()` — and correctly **deferred** (JIT-discovered via the Tool Search Tool), not eagerly loaded into context, which is exactly the anti-overwhelm posture: the agent discovers and calls it only when it needs a node's connections. This pairs directly with the graph-aware retrieval shipped in #232: `createContextPack` hands the agent a budgeted slice plus expandable node ids; `xnet_graph_expand` is how it pulls a specific node's connections on demand instead of over-fetching the whole graph. ## Verification - 4 new tests (30 in `ai-surface.test.ts`): outbound walk, not-found, limit, tool-list presence. - `tsc --noEmit` clean for `@xnetjs/plugins` (turbo); eslint + prettier clean. - `fallow audit` (with coverage): *"✓ No issues in 2 changed files"* — the new functions add zero enforced complexity findings. - No new dependencies (uses the service's existing `store` + `schemas`). ## Still deferred (0211 doc stays `[_]`) The in-app **vector tier** (waking `@xnetjs/vectors` behind the retriever seam — a perf-sensitive change better suited to its own PR, possibly via a managed server-side embedding route given browser cost), the `data-bridge` query-path placement of the locality planner, `WorkingSetPrewarm` consumption, and a managed `/ai/embed` hub route. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jun 22, 2026
…ff) (#236) The final piece of exploration [0211](docs/explorations/0211_[x]_AI_SECOND_BRAIN_GRAPHRAG_MEMORY_AND_TIERING.md) — wakes the dormant `@xnetjs/vectors` engine behind the retriever seam, making the **semantic half** of the AI second brain live. After #228 (engine), #230 (seam), #232 (graph-aware retrieval live), and #233 (`xnet_graph_expand`), this flips 0211 to **`[x]`**. ## What ships **`ai-vector-search.ts` — `createVectorEntrySearch`** (opt-in, lazy, fallback-safe): - The heavy `@xenova` model + `usearch` load via a **dynamic import only on the first search after opt-in** — zero boot/bundle cost when off (the [0204](docs/explorations/0204_[x]_FAST_LOCAL_FIRST_COLD_START_AND_CACHE_HYDRATION.md) constraint). - Once warm it **RRF-fuses vector + keyword**; until warm and on **any** failure it falls back to keyword — so enabling it can only ever match or improve results, **never break or slow** the chat. - **`ai-vector-storage.ts`** — IndexedDB `BlobStore` so the index **restores across sessions** instead of re-embedding the graph each time. - **`AiChatPanel`** — an opt-in toggle (off by default); the flag is read via a ref so toggling **never tears down the AI runtime** mid-conversation. ## Adversarial review caught 3 real bugs (fixed here) A 22-agent review (18 findings → 15 false positives killed by verification → 3 confirmed) surfaced: 1. `loadVectorTier` returned `false` on a *partial* restore but left the index half-populated → now **clears on failure** so "false ⇒ cold" is honest. 2. Persistence was never wired (no `storage`) → re-embedded every session and the realm-fix was dead in prod → now **wired via IndexedDB**. 3. Toggling the flag rebuilt `surface` → **reset the active thread** mid-conversation → now decoupled via a ref. Plus a realm-robustness fix in `brain/persist.ts`: a plain `instanceof Uint8Array` mangles bytes across the dynamic-import / IndexedDB structured-clone boundary; the `Symbol.toStringTag` brand holds. ## Verification - **End-to-end in the real browser**: warm → hybrid hit (`source: "hybrid"`); IndexedDB persist → restore (no re-embed) → query found the node. The cross-realm persist fix exercised under real production conditions. - 17 unit tests (vector tier lazy/fallback/persist + the persist clear-on-failure regression + graph retriever); `tsc` clean for `@xnetjs/brain` + `xnet-web`; eslint + prettier clean; `fallow audit` — *"✓ No issues in 10 changed files."* ## Justified defers (documented in the 0211 doc; none block the core) - **Managed `/ai/embed` route** — the server-side embedding upgrade path; dead infra until demand, so it follows demand. - **data-bridge locality** — *blocked*: `apps/web` never instantiates a `remoteNodeQueryClient`, so there's no remote read path to route to. - **`WorkingSetPrewarm` planner scores** — marginal until per-node frequency/pinned signals are tracked at boot (it already ranks by recency). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Phase 3 of exploration 0211 — makes the brain live in the app, building on the engine (#228) and the
AiSurfaceServiceseam (#230).What ships
The AI chat's context pack now comes from
@xnetjs/brain'sretrieve()instead of a flat keyword scan.apps/web/.../ai-graph-retriever.ts(new, 6 tests) —createGraphContextRetrieverbuilds anAiContextRetrieverover the local NodeStore: keyword entry search → bounded expansion along typed relations (resolved from the schema registry) → token budget → each hit carries a readable provenance path.AiChatPanelinjects it:createAiSurfaceService({ store, schemas, retrieveContext: createGraphContextRetriever(store) }). SocreateContextPack({ query })(already called per message) now walks the graph.AiContextRetriever/AiRetrievedNodefrom the@xnetjs/pluginsbarrels;@xnetjs/brainadded toapps/webdeps.Deliberately model-free
Entry search is keyword-only — no embedding-model download, no added bundle weight or startup cost (the 0204 cold-start constraint). The value-add here is the graph-walk + budget + provenance. The vector tier can swap in behind the same seam later without touching this call site.
Verification
@xnetjs/brainexercises (browser runtime, via Vite module graph):retrieve('inventory')returned the entry node plus its 1-hop neighbor with path"My inventory→ (items) Sword of testing".ai-surfacetests green;tsc --noEmitclean for@xnetjs/plugins+xnet-web(40 turbo tasks); eslint + prettier clean.Still deferred (future enhancements, doc stays
[_])Swap the vector tier in behind the seam (wake
@xnetjs/vectorsin-app + backfill/persist), anxnet_graph_expandMCP tool, thedata-bridgequery-path placement of the locality planner,WorkingSetPrewarmconsumption, and a managed/ai/embedhub route.🤖 Generated with Claude Code