Skip to content

Release 2.10.1 — local-first performance#69

Merged
acidkill merged 7 commits into
mainfrom
fix/schema-flexible-schemaless-2101
Jul 13, 2026
Merged

Release 2.10.1 — local-first performance#69
acidkill merged 7 commits into
mainfrom
fix/schema-flexible-schemaless-2101

Conversation

@acidkill

Copy link
Copy Markdown
Owner

Summary

Performance + correctness patch for the SurrealDB backend, focused on large brains and local (self-hosted) embedding. No schema migration; recall ranking is unchanged unless you change your embedding config.

Five changes:

  1. feat(embedding): configurable local embedding endpointSURREAL_MEMORY_EMBEDDING_ENDPOINT lets the openai provider target a local OpenAI-compatible server (bge-m3 via llama.cpp / llamastash), mirroring the reranker's SURREAL_MEMORY_RERANKER_ENDPOINT. Placeholder api-key for keyless local servers.
  2. feat(encode): inline embedding on write — freshly-saved memories are embedded as part of the save instead of only on the next batch smem reindex, so semantic recall works immediately. Fail-soft (embeddings off / no provider → keyword-only, no error, no slowdown).
  3. fix(consolidation): prune N+1 timeoutprune bridge-detection issued one query per candidate source neuron (tens of thousands on a 66k-neuron brain), blowing the 120s per-strategy budget. Now groups the already-loaded synapses in memory (0 extra queries); read path ~110s → seconds.
  4. fix(surrealdb): SCHEMALESS metadata tables — the 7 arbitrary-metadata/config tables are declared SCHEMALESS (accept nested keys without FLEXIBLE), stopping FLEXIBLE can only be used in SCHEMAFULL tables errors on databases whose tables were first created SCHEMALESS. synapse + retrieval_trace stay SCHEMAFULL.
  5. fix(hooks): stop-hook recognizes local embedder — session-end semantic dedup treats a loopback SURREAL_MEMORY_EMBEDDING_ENDPOINT (llamastash bge-m3) as local-and-cheap instead of falling back to simhash.

Verification

  • 162 encoder/pipeline unit tests pass; inline-embed is fail-soft (no provider → early return).
  • prune: 46 consolidation/perf/pinned tests pass; in-memory grouping measured 0.04s vs ~100s N+1.
  • schema: full SCHEMA_SQL applied to fresh + legacy-schemaless DBs → 0 FLEXIBLE errors, nested metadata writes work.
  • inline-embed: verified against a live 66k-neuron brain — a permanent remember now stores a 1024-dim embedding_vec (was NONE).

Test plan

  • CI green (lint, mypy, tests 3.11+3.12, docs freshness, security, build)
  • After tag: verify PyPI + both npm packages published against live registries

Toni Nowak added 7 commits July 12, 2026 19:24
… schema errors on legacy DBs

The 7 tables carrying an arbitrary-key metadata/config object (neuron, fiber,
brain, typed_memory, source, alerts, brain_versions) were declared SCHEMAFULL
with FLEXIBLE object fields. On any DB whose tables were first created SCHEMALESS
(every pre-existing/upgraded install), the non-convergent `DEFINE TABLE X
SCHEMAFULL` is swallowed as already-exists, so the table stays SCHEMALESS and the
following `DEFINE FIELD ... FLEXIBLE` fails every ensure_schema()/consolidate with
'FLEXIBLE can only be used in SCHEMAFULL tables' (8 warnings per run).

Converting those live tables to SCHEMAFULL is unsafe: it bricks UPDATEs on any row
holding a legacy field absent from the schema (verified). A SCHEMALESS table already
accepts arbitrary nested object keys WITHOUT FLEXIBLE, so declare these 7 tables
SCHEMALESS and use plain `TYPE object` — correct and identical on fresh and upgraded
DBs. synapse + retrieval_trace stay SCHEMAFULL (a migration drops & recreates them,
so their FLEXIBLE fields are always valid).

Verified: applying the full SCHEMA_SQL to a fresh DB and to a legacy-schemaless DB
both yield 0 FLEXIBLE errors and accept nested metadata writes.
…eout on large brains)

_prune fetched per-source outgoing synapses via storage.get_synapses_for_neurons()
for tens of thousands of candidate source neurons. The SurrealDB backend does not
override that method, so the base implementation fanned out into one query PER
neuron — ~50k sequential round-trips on a 66k-neuron / 390k-synapse brain — blowing
the 120s per-strategy prune budget (consolidate reported 'prune timed out after
120.0s').

prune already holds every synapse in all_synapses, so build the source->synapses
map in-memory (O(n), measured 0.04s) instead of asking storage. Same bridge-detection
semantics (map values = all outgoing synapses per source). Read-path drops from
~110s (N+1) to ~15s. 46 consolidation/perf/pinned unit tests pass unchanged.
…DING_ENDPOINT

The reranker can be pointed at a local OpenAI-compatible server via
SURREAL_MEMORY_RERANKER_ENDPOINT, but the embedder had no equivalent — the
openai-provider factory sites all construct OpenAIEmbedding(model=...) with no
base_url, so a local bge-m3 (llamastash, http://127.0.0.1:11435/v1) could not be
targeted without hijacking the shared OPENAI_BASE_URL.

OpenAIEmbedding.__init__ now defaults base_url from SURREAL_MEMORY_EMBEDDING_ENDPOINT
when none is passed, and supplies a placeholder api-key for keyless local servers.
Every factory site (semantic_discovery, dashboard_api, stop-hook) picks this up with
no further change. Added bge-m3/bge-large (1024) to the known-dimensions map.
…le embedder

The stop hook's semantic-dedup fast-path only treated 'ollama' as a cheap local
embedding provider and skipped everything else to stay fast. A local llama.cpp /
llamastash bge-m3 served over the OpenAI-compatible API (127.0.0.1:11435, ~15ms/
embed on GPU) is just as cheap, but was being skipped — so session-end dedup fell
back to simhash only. Recognize provider openai/openrouter with a loopback
SURREAL_MEMORY_EMBEDDING_ENDPOINT as local-and-cheap; remote/cloud bases stay
skipped to keep the hook fast.
… works immediately

Embeddings were only ever populated by a batch `smem reindex`, so a just-saved
memory was keyword-recallable but NOT semantically recallable until the next
reindex — 41 neurons created in a 2h window had zero embedding_vec. Root cause:
the encode pipeline never calls the embedder; add_neuron only stores a vector if
the neuron already carries `_embedding` in metadata, which nothing on the write
path set.

encode() now finalizes with _embed_created_neurons(): one embed_batch over the
non-ephemeral, non-TIME, content-bearing neurons it created, then update_neuron
with the vector. Fail-soft — embeddings disabled or provider unavailable → memories
stay keyword-only (no error, no slowdown), exactly the prior behaviour, so tests
and offline setups are unaffected. With a local endpoint (bge-m3 via llamastash,
~15ms) the added cost is small.

Verified against a live 66k-neuron brain: a permanent remember now stores a
1024-dim embedding_vec on the created neurons (was NONE); 162 encoder/pipeline
unit tests pass unchanged (fail-soft with no provider in the test env).
Bump 2.10.0 -> 2.10.1 (pyproject, __init__, npm + vscode manifests) and document the
2.10.1 changes: configurable local embedding endpoint (SURREAL_MEMORY_EMBEDDING_ENDPOINT),
inline embedding on write, prune N+1 timeout fix, SCHEMALESS metadata tables, and stop-hook
local-embedder recognition.
…bump version test; format

- OpenAIEmbedding supplied a placeholder api-key whenever ANY base_url was set,
  which included subclasses passing their own remote base_url — OpenRouterEmbedding
  (base_url=openrouter.ai) stopped requiring a key, breaking test_requires_api_key.
  Gate the placeholder to the local-endpoint env knob only (SURREAL_MEMORY_EMBEDDING_
  ENDPOINT + no explicit base_url), so remote subclasses still require a real key.
- Update test_version_is_current to 2.10.1.
- ruff format encoder.py.
@acidkill
acidkill merged commit 1444d37 into main Jul 13, 2026
7 checks passed
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