Titentiten.devꦠꦶꦠꦺꦤ꧀
v0.1.1 · npmGitHub
DocsDeploySemantic vectors

Semantic vectors

Live on Bun and SQLite through sqlite-vec plus any OpenAI-compatible embedding endpoint; the index adds recall and is never the source of truth.

sqlite-vec 0.1.9no required depsretrievable claims onlyown database file

Semantic retrieval is live on Bun with SQLite: sqlite-vec stores and searches the vectors, and any OpenAI-compatible endpoint produces them. Titen never ships a model.

Runtime Store Embedder State
Bun + SQLite sqlite-vec in its own database file any OpenAI-compatible /embeddings endpoint Live, verified with embeddinggemma at 768 dims
Cloudflare Workers Vectorize Workers AI Planned. The adapter exists; no bindings are declared, so retrieval is lexical FTS5

Turn it on

Vector settings come from the environment, not from flags, because one of them is a credential and belongs in a unit file rather than a shell history.

/etc/systemd/system/titen.service.d/vectors.confini
[Service]
Environment=TITEN_EMBED_BASE_URL=http://127.0.0.1:11434/v1
Environment=TITEN_EMBED_MODEL=bge-m3
Environment=TITEN_EMBED_DIMS=1024
Environment=TITEN_VEC_DB_PATH=/var/lib/titen/vectors.db
ENVIRONMENT

TITEN_EMBED_BASE_URL required; the request goes to this value plus /embeddings

TITEN_EMBED_MODEL required; passed through as the request's model

TITEN_EMBED_DIMS required; every response is checked against it

TITEN_EMBED_API_KEY optional; sent as a bearer token when set

TITEN_VEC_DB_PATH optional; defaults to the canonical path plus .vec

All three required values must be present or the capability is not built and /readyz reports vector: "disabled". The prebuilt extension is per-platform: a platform without one degrades to FTS instead of refusing to start.

The index is an index

Vectors live in a separate database file from canonical records. Lose vectors.db and no evidence is lost: a fresh index file is created on the next start. Lose the canonical database and nothing brings the evidence back.

A vector hit only nominates claim ids. Eligibility is decided afterwards by the same organization, subject, project, status, temporal and visibility predicates the lexical path uses, so semantic proximity widens what is found, never what may be read.

Canonical export carries no embeddings: changing embedding provider changes retrieval scores without touching evidence, claims or record ids.

Trust, temporal validity and conflict state are still checked from canonical SQL after every candidate lookup.

Rebuildable is not self-healing

Outbox rows are queued at write time only, so claims written before an index was lost are not re-embedded on their own, and no re-index command ships today. Consolidating a claim again queues it. Compile degrades to lexical for anything missing from the index.

Only retrievable claims get embedded

The semantic unit is a retrievable claim: active or disputed, not every raw chat turn. Embedding raw turns inflates the index and raises the odds that stray instruction-shaped text dominates similarity.

An observation append and a consolidation each queue a row in index_outbox. The drain embeds one batch, writes the index, and only then marks rows done, so a model outage leaves them queued instead of dropping them. Non-claim rows are retired immediately, and so is a claim that was superseded, revoked or expired since it was queued.

curl -sX POST -H "authorization: Bearer $TITEN_API_KEY" \
  'http://127.0.0.1:8787/v1/index/drain?limit=50'

The response reports drained, indexed, skipped, remaining, plus the model and dimensions it used. With an in-process maintenance timer you never call this; see background repair.

Hybrid ranking and normalization

The index adds recall: it runs even when lexical search matched nothing, because the memory worth retrieving is often phrased nothing like the question. A semantic score then competes with the lexical one as relevance, max(lexical, semantic), instead of adding a sixth weighted term to the five in score_components. With no vector capability the value is zero and ranking is arithmetically identical to lexical-only.

Both signals are normalized inside the candidate set before they compete, because real embedding models return cosines in a narrow absolute band and it is the spread that carries the signal. Against embeddinggemma, three claims spanned 0.437 to 0.463; passing those raw left an exactly-matching claim ranked last, because the confidence multiplier swung the composite far more than a 0.026 difference could.

Scores are for ordering, not comparison

The sqlite-vec store derives its score from L2 distance as 1 / (1 + d). That is monotonic (nearer always scores higher) but not a calibrated similarity, so a score from one query means nothing next to a score from another.

What degrades

Situation What happens
No embedding configuration vector: "disabled", model: "disabled"; retrieval is FTS5
Extension will not load on this platform Capability is not built; readiness reports it disabled
Model unreachable during compile Pack still returns lexical results; meta.degraded.vector: "error"
Model unreachable during drain 503 UNAVAILABLE, retryable: true, pending count reported, queue unchanged
Drain called with no capability 400 VALIDATION_ERROR — “No vector capability is configured on this deployment.”
Endpoint returns the wrong width The embedder throws embedding dimension mismatch: expected 1024, got 768

Compile never fails because a model failed. It degrades and says so in the same response:

"degraded": { "semantic": false, "vector": "used", "model": "enabled" }

vector is "used" when the index answered, "error" when the call failed, and "disabled" when no capability is configured.

/readyz reports the store and the model as separate facts: a deployment can have an endpoint with no working index, or an index with no reachable endpoint.

Planned: a dimension check at readiness

The dimension is validated on every embedding response, so a misconfiguration surfaces on first use. A readiness check for a dimension mismatch is recorded as the upgrade path in src/core/vectors.ts. Until it exists, treat a change of model or dimension as an explicit re-index.

Next

↑↓ navigate↵ open