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

Semantic vectors

Live on Bun and SQLite through sqlite-vec plus any OpenAI-compatible embedding endpoint — an index that adds recall and can never become 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: one is a credential, and the rest are deployment facts that belong in a unit file.

/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 at all and /readyz reports vector: "disabled". Partial configuration never half-enables retrieval. 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, and that is the load-bearing invariant rather than tidiness. 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 can widen what is found and never what may be read.

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

A high similarity score is not authority. 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, which is the available repair. Compile degrades to lexical for anything missing from the index rather than returning nothing.

Only retrievable claims get embedded

The semantic unit is a retrievable claim — active or disputed — not every raw chat turn. Claims are compact, typed, temporal and linked to evidence; 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 so the queue cannot grow without bound, and so is a claim that was superseded, revoked or expired since it was queued — it is no longer retrievable.

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. On a deployment with an in-process maintenance timer you never call this — see background repair.

Hybrid ranking, and why normalization matters

The index contributes recall, not only re-ranking: 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) — rather than becoming a sixth weighted term. 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 it is not a calibrated similarity, so a score from one query means nothing next to a score from another.

What degrades, and how you see it

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, because a deployment can have an endpoint with no working index or an index with no reachable endpoint, and a single merged flag would hide both.

Planned: a dimension check at readiness

The dimension is validated on every embedding response, which catches a misconfiguration 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