# Engine changelog

Source: https://docs.littlebigbrain.com/changelog/engine/

Updates to the database engine, query APIs, and developer tools. For the hosted
console, accounts, and billing, see the [Cloud changelog](https://docs.littlebigbrain.com/changelog/cloud/).

## Unreleased

### Questions get 60 seconds

`POST /v1/query/ask` now stops at a 60-second read deadline. Other reads keep
25 seconds. A question that needs several model turns and queries now has time
to answer.

While the model writes a turn, the question holds no server slot. Other
requests can run in that time. If the server stays full after a turn, the loop
stops and returns the best rows it read. Its `error` says the server stayed
busy. No client change is needed. The SDKs already wait up to 120 seconds.

### Keep a generated file

`DELETE /v1/files/expiry?sha256=` removes the `expires_at` of a generated file,
one that a workflow run wrote. The file then stays until a person deletes it,
and `GET /v1/files` lists it. A repeat answers the same record. An upload of
those bytes answers `409 file_generated`. The bytes are not a source. No route
writes a generated file yet.

### Commits stay inside what a workflow owns

A commit under a workflow turn must write only the properties its workflow
lists in `owns.properties`. A property outside them answers
`403 write_outside_owns` with the IRIs in `details`, and nothing is written. A
`ValueSource` entity may carry `property`, `value` and `support` whoever the
owner is. A commit with `property_merge: "replace"` is refused too. Replace
erases fields of the entity that the workflow may not own.

At first the server writes such a commit as before. It only counts what it
would refuse. No client change is needed.

### Check proposed values against their files

`POST /v1/workflows/activities/ground` checks values that a model proposes.
Each item names a file, a place in it, a property, a value and a quote. The
server answers one verdict per item, in order, with no model call.

- **Checks.** The file is live and its upload is the one the item read. The
  place exists. The quote is in the text there. The property is in the
  ontology. The value takes its type, and a number comes in the unit of its
  property, converted with a fixed factor.
- **Places.** `page=N` for a parsed PDF, `line=a,b` for text and Markdown,
  and `row=a-b` for CSV and TSV.
- **Evidence.** A grounded item carries the typed value and the evidence of
  its quote, with the box of the lines on a PDF page.
- **Bounds.** A request holds 16 items from 8 files. It scans 4 MiB, and an
  item scans 1 MiB of its file. A PDF's stored pages may hold 16 MiB. A place
  past a bound gets `scan_budget`. A request past 10 seconds answers
  `503 ground_timeout` with no verdict.
- **Values.** A value is at most 100 bytes and a unit 24 bytes. Only a
  number is read apart from its unit. A text keeps its words.
- **Open to all.** The route calls no managed model, so Free accounts use it.

### Rust source convenience wrappers

Remove the uncalled Rust helpers `lbb_fts::search_bm25` and
`GraphStore::warm_object`. This is a downstream Rust source-API break for Git,
path or other crate consumers; external use is unknown. The retained
`Bm25Index::build(documents)?.search(query, top_k)` performs the same legacy
BM25 operation. Current operator warming keeps its bounded graph plan; it is
not equivalent to an arbitrary-key whole-object read.

No HTTP/SDK route, package version, persisted format, current RDF/class embedding
search or shared ANN/reranking/Ask behavior changes. This source notice does not
announce package publication, deployment or completion of a retirement window.

## October 2026

### 28 October: Hosted workers parse files in a parse slot

On a hosted cell the workers no longer start `pdftotext` and `pdfimages`.
Each worker process has a parse slot: a container with its own user, its own
process view, no network, a read-only root and a 16 MiB work directory. The
slot runs the program with an empty environment, no capability, and limits on
memory, CPU time, file size, open files and processes. It kills the program at
its wall limit and empties the work directory after every file.

- **Checked result.** The worker checks what the slot returns before it stores
  a page: valid UTF-8, at most 2,000 pages in order, a page size that is a
  number of points, line ids and texts that match their words, and boxes from 0
  to 1. A result that fails a check is a parse failure and stores nothing.
- **Output cap.** A file whose text output passes 128 MiB now fails with
  `the parser wrote more than 134217728 bytes`. The cap was 256 MiB.
- **New settings.** `LBB_PARSE_MODE` and `LBB_PARSE_SLOT_DIRS` for the worker.
  `LBB_SERVING_PARSE` for the server: `off` makes the serving, ingest and
  maintenance processes start no Poppler. It defaults to `on`.
- **Roll order.** Roll the pool: the slots start first, then the workers.
  Turn `LBB_SERVING_PARSE` off only after the workers parse. `roll-cell.sh`
  refuses a deploy that does not set `LBB_WORKER_WORKFLOWS` to include
  `documents.parse`.
- **Not covered yet.** The XLSX reader still runs in the serving process.

### 27 October: Read XLSX workbooks by sheet and cell

`GET /v1/files/sheets` serves an XLSX workbook. It gives the outline of the
sheets, or the rows of one sheet by A1 row. See
[Spreadsheets](https://docs.littlebigbrain.com/guides/documents-to-graph/#spreadsheets). A cell has the
value its file stores and its formula apart. LBB calculates nothing.

- **Bounded read.** The first read parses the workbook in a child process.
  The child has an empty environment.
- **Hard limits.** A limit refuses the whole workbook with `xlsx_refused`.
  The limits cover the file, its expansion and its XML elements. They cover sheets, rows, columns, cells and cell text. They also cover the output and the CPU time.
- **Few parses at a time.** A node parses two workbooks at once, and a tenant
  one. The parse ends with the read that asks for it.
- **New codes**: `xlsx_refused`, `xlsx_busy`, `file_not_spreadsheet` and
  `sheet_not_found`.
- **Places.** Evidence names a place in a sheet with `…#Sales!B2:D9`. The
  console opens the file at it. The `rows` template reads XLSX files and
  writes `#Sales!A5:F5` for a row.
- **Server setting.** `LBB_PYTHON3` names the interpreter. The server image
  installs `python3`.
- **SDKs.** `files.sheets` in TypeScript and Python. Helpers write and read
  the fragment of a place.

### 27 October: Grade the values no label judges

Sometimes the arms of an experiment differ on a value that the dataset does
not label. A person can now grade that value in the Review inbox. The
answer becomes a label of the dataset
([Grade the values no label judges](https://docs.littlebigbrain.com/guides/experiments/#grade-the-values-no-label-judges)).
Little Big Brain files these items only when the operator turns the feature
on.

- **New review origin** `evals:datasets`, kind `grade`, options `right` and
  `wrong`. `ReviewEvalOrigin` has the kind `dataset` and the fields `item`,
  `property`, `value` and `stated`. `eval_id` filters by dataset.
- **New field** `source` on a label of a dataset item: `{kind, id, at}`. The
  `kind` is `test` or `review`. The latest answer of a person wins. A label
  without a `source` is never replaced.
- **Changed scoring.** A property with only `leave_out` rows of values judges
  those values. Every other value stays unlabeled. Before, it was `wrong`.
  A report scored before this release reads as stale, because `labels_etag`
  now ends in `#g2`. The next score turn scores it again.
- **New error code** `eval_dataset_items_too_large` (400): a label would make
  a dataset's items larger than 8 MiB.
- **New flag** `LBB_REVIEW_EVAL_DATASETS` (`off` by default; needs
  `LBB_REVIEW_EVAL_ITEMS`). It gates filing, never an answer.
- **New metrics** `lbb_review_eval_datasets_enabled{mode}` and
  `lbb_review_eval_items_waiting{origin="dataset", reason}`. The counter
  `lbb_review_eval_items_total` has the origin `dataset`.
- **Roll first**: an earlier node reads a dataset item as `unknown`. It
  answers the item with no label and drops `source` from labels. Roll every
  node before you turn the flag on.

### 27 October: Scanned pages are flagged

A scanned PDF page is a picture of text. Its text layer is empty or nearly
so. The `documents.parse` workflow now flags such a page `ocr_required: true`.
A page gets the flag when two things hold. It has fewer than 16 characters of text. Its images cover at least 40% of it.

- **New fields**: `ocr_required` on a page of `GET /v1/files/pages`. Also
  `ocr_pages` in the `parse` of a file and on its `Document`.
- **Parser id**: pages with the flag are stored under
  `poppler-<version>-scan1`. Pages parsed before keep their id and no flag.
- **Console**: a page that needs OCR shows a warning. The file list shows
  how many pages need OCR.
- **Run results**: with the Rust workflow, a file of which every page needs
  OCR is a result with no value. OCR itself is not built.
- **New setting**: `LBB_PDFIMAGES` names the Poppler program that lists the
  images. The server image and the worker image already contain it.
- **One file cannot hold the queue.** A file whose parse stops goes to the
  back. At its third stop it is stored as `failed`, with the reason.
- **Roll order**: roll the server first, then the workers. An old server
  drops the flag and stores pages without it under the new id. The new
  server then answers `409` for the same pages with the flag.
- **Parse programs**: `pdftotext` and `pdfimages` now run with an empty
  environment and a fixed `PATH`. No key or token of the server or the worker
  reaches them. They also run with limits on memory, CPU time and file size.
  Before, they inherited the environment of their host. The server set no
  limit.

### 27 October: Retry a failed reading once

`values.extract` takes the parameter `repair`. With `repair: 1`, an input whose
first answer fails a check gets one more attempt. The retry sends the same text
and the failed checks. See
[Retry a failed reading once](https://docs.littlebigbrain.com/guides/starter-workflows/#retry-a-failed-reading-once).
`repair: 0` is the default and changes nothing.

- **New parameter** `repair` (`0` or `1`). It is part of the settings
  revision. A test on a named revision reads it from there.
- **New fields**: `attempt` on a logged call and on a call list row (`2` on the
  retry). `attempt` on a value of a test or run result. `settings.repair` on a
  test. `attempt` on a `reason` activity request.
- **New counter** `retried` in the workflow's status.
- **Roll first**: an earlier node refuses `repair` on a PUT. It also refuses
  `attempt` on a `reason` request.

### 27 October: See whether a revision was tested

A revision of a `values.extract` workflow shows whether a workflow test read
inputs with exactly its settings
([See whether a revision was tested](https://docs.littlebigbrain.com/guides/workflow-settings-history/#see-whether-a-revision-was-tested)).

- **New field** `last_tested` on `GET /v1/workflows/named/revisions` and
  `…/revisions/get`: `{test, finished, counts}` for the newest test that
  counts, `null` when no test is recorded for these settings, and no key for a
  workflow without tests. An experiment's arm is such a test and adds
  `experiment` (`id`, `arm`). It shows what happened and gives no verdict.
  Activation is never refused.
- A test counts when it ended `done` and the model read at least one input.
  Tests from before this release do not count. In a model experiment only
  arm A records a fact, because B runs another model than the workflow's.
- The console marks each row of the history and warns before it uses
  settings with no recorded test.

### 26 October: Compare two versions of a workflow

An **experiment** runs a `values.extract` workflow twice on the items of a
dataset: as it is (A) and with one change (B). B is the model of one step
(`extract` or `decide`) or a kept settings revision. Each output is checked
against the dataset's values. The report gives the difference with a 95%
interval ([Compare two versions of a workflow](https://docs.littlebigbrain.com/guides/experiments/)).

- **New routes** `POST /v1/evals/experiments`, `POST
  /v1/evals/experiments/estimate`, `GET /v1/evals/experiments`, `GET
  /v1/evals/experiments/get` and `POST /v1/evals/experiments/cancel`.
- **New fields** `experiment` on `PUT /v1/models/choices` and on a revision
  activation. The write applies the experiment's B, and the server refuses
  it with `experiment_outdated` when the workflow no longer runs A.
- **New codes** `experiment_arms_mixed`, `experiment_outdated` and
  `experiments_instance_taken`.
- An experiment writes nothing to the graph and changes no workflow. Its
  model calls count in the environment's monthly workflow budget, as a
  test's do.

### 22 October: Choose the model of each workflow step

The AI steps of workflows can run another model, for the whole environment
or for one workflow
([Choose the model of a workflow step](https://docs.littlebigbrain.com/guides/check-model-quality/#choose-the-model-of-a-workflow-step)).
`extract` is the reasoning model (`values.extract` reads values, the
`reason` activity), and `decide` the decision model (the support of a
value, the `decide` activity).

- **New routes** `GET`, `PUT` and `DELETE /v1/models/choices`, with the
  codes `model_choice_unsupported` and `model_choice_unavailable`.
- **New fields**: a logged call's `choice` (why it ran its model),
  `models` on a test create to test other models, and a test's `models`.
- A run's fingerprint names its models, so runs before and after a change
  have different fingerprints.

### 22 October: A workflow's settings keep their history

Each change of a workflow's settings is a revision, named by a hash of the
settings: the same settings always get the same id. A workflow's history
lists who changed its settings, when, and what changed, and an earlier
revision can be used again
([Settings history of a workflow](https://docs.littlebigbrain.com/guides/workflow-settings-history/)).

- **New routes** `GET /v1/workflows/named/revisions`, `GET
  /v1/workflows/named/revisions/get` and `POST
  /v1/workflows/named/revisions/activate`, with the codes
  `workflow_revision_conflict`, `workflow_revision_stale` and
  `workflow_revision_not_found`.
- **New fields**: a workflow's `active_revision`, a run summary's and a
  test's `revision`, and `revision` on a test create, to test earlier
  settings without using them.
- `PUT /v1/workflows/named` and `PUT /v1/workflows/triggered` keep their
  bodies and answers.

### 21 October: Every error code has an entry, and a lost turn lease has its own code

Each error body's `doc_url` now links to the code's entry on
[Error codes](https://docs.littlebigbrain.com/sdks/errors/): its status, its type, what it means and what
to do. Each code the data plane sends has one entry, and CI checks it.

What changes in the error bodies:

- **New code `turn_lease_lost`** (409, `conflict_error`, `retryable: false`,
  `param: "token"`). A `/v1/workflows/turns/*` route answers it when the
  token no longer holds the turn: another attempt holds it, its lease ended,
  it waits after a sleep or signal checkpoint, or it finished. Before, these
  answered `409 conflict`. The SDK workers stop on both codes. A worker that
  calls these routes itself and stops on `conflict` must stop on
  `turn_lease_lost` too.
- **A 402 without a code of its own** answers `quota_exceeded`
  (`quota_error`). Before, its code was `api_error`, the name of a type.
- **`param`** names the field to change on more errors: a request body's
  missing, unknown or doubled field, a missing query parameter of the
  workflow, starter and evals routes, the review list's `cursor`, and the
  review answer's `Idempotency-Key`.
- **`request_id`** is in every error body, and the `X-Request-Id` header has
  the same value, also on a request refused before it reaches a route (a bad
  header, a busy server). A path that no route serves answers `404
  not_found` in the error shape, not as text.

No code changed its type: a code keeps the type it was first sent with.

### 5 October: Questions get one route, `POST /v1/query/ask`

`POST /v1/query/ask` answers a question in plain words. A model runs
queries in a short loop, reads the rows, and answers with the records it
cites.

```sh
curl -X POST "$LBB_URL/v1/query/ask?graph=services" \
  -H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
  --data '{"question": "How many services does each team run?"}'
```

This change breaks clients of `POST /v1/query/rewrite`:

- `POST /v1/query/rewrite` is gone. It answers 404 and names the new route.
- The one-shot mode is gone: `mode: "rewrite"`, `run` and `previous` answer
  400. The loop always runs its queries and corrects them itself.
- `mode: "route"` stays: the route alone, in about 0.3 seconds.
- The stream events `query`, `run`, `rows` and `repair` are gone. The stream
  sends `step` per tool call, then `answer`.
- `history.before` and `history.after` are gone. A comparison fills
  `history.added`, `history.removed` and `history.changed`.
- The loop's steps include `compare`, for "what changed" questions, and a
  `describe` that shows how many records hold each property and the values
  of small classes.

The SDKs change with it. TypeScript has `query.ask` and `query.askStream`,
Python `query.ask` and `query.ask_stream`. `query.rewrite` and the rewrite
streams are gone. MCP `lbb_query mode=question` always answers. To write
queries in your own loop, use
[the tools](https://docs.littlebigbrain.com/guides/answer-questions/#use-the-tools-in-your-own-agent).

### 5 October: Rerank inside a SPARQL query

A SPARQL search can now rerank its hits. Add `search:rerank true` to the
search triples.

- The rerank model reads the best hits with the query text. The query keeps
  the hits that answer it best.
- `search:relevance ?r` binds the model's answer, from 0 to 1. Order by it
  with `ORDER BY DESC(?r)`.
- A query reranks only when it asks. The graph's rerank setting applies to
  `POST /v1/search` only.
- See [Rerank inside a query](https://docs.littlebigbrain.com/guides/search-by-meaning/#rerank-inside-a-query).

### 4 October: Rerank search hits

A search can now rerank its best hits. Jev, a decision model, reads each hit
with your words. The hits that answer them come first.

- Turn it on for a graph with `PUT /v1/search/settings` and
  `{"rerank": true}`. One search can set `rerank` for itself.
- Each hit gets `relevance`, the probability that it answers the words.
  `score` stays the similarity.
- On BEIR SciFact, nDCG@10 rose from 0.713 to 0.797 over 300 test queries.
  A rerank adds about 0.3 s.
- A rerank never fails a search. Without an answer in time, the hits keep the
  similarity order.
- The SDKs add `searchSettings()` and `setSearchSettings()`
  (`search_settings()` and `set_search_settings()` in Python). MCP's
  `lbb_query mode=search` takes `rerank`.

See [Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/#rerank-the-best-hits).

### 4 October: Questions in plain words become SPARQL queries

`POST /v1/query/rewrite` turns a question into one checked SPARQL query. With
`run: true`, it also runs the query and returns the rows.

```sh
curl -X POST "$LBB_URL/v1/query/rewrite?graph=services" \
  -H "Authorization: Bearer $LBB_API_KEY" -H 'Content-Type: application/json' \
  --data '{"question": "How many services does each team run?", "run": true}'
```

- The server describes your graph: classes, properties, schema statements
  and the classes you can search by meaning.
- TypeSafe's Jev picks the kind of query: lookup, aggregate, search, history,
  schema or unanswerable. Claude Sonnet 5.5 writes the query.
- The server checks the query and adds missing `PREFIX` lines. A query that
  fails is corrected once.
- A run records an eval trace with the question as its request.
- `previous` carries earlier attempts, `context` carries your app's notes,
  and `mode: "route"` returns the kind of query only.
- TypeScript and Python add `query.ask` and `query.rewrite`. MCP `lbb_query`
  adds `mode: "question"`.

LBB runs both models with its own keys. The question, your notes and a
description of the graph, with short example values, go to the model
providers. See [Answer questions in plain words](https://docs.littlebigbrain.com/guides/answer-questions/).

### 3 October: Search by meaning inside SPARQL

A SPARQL query can now search by meaning. Any other pattern on the hit
filters it: a link, a value, a date or a type.

```sparql
PREFIX search: <https://littlebigbrain.com/search#>
PREFIX dr: <https://littlebigbrain.com/r/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?x ?score WHERE {
  ?x search:similarTo "card payments" ; search:score ?score .
  ?x dr:calls ?callee . ?callee rdfs:label "payment-service" .
}
ORDER BY DESC(?score) LIMIT 5
```

- The query is a text, a record ("more like this") or your own vector.
- The engine picks a plan by cost. A narrow filter is read first, and only
  its records are scored. A broad filter is applied to the nearest part of
  the index.
- The response's `search` object names the plan, both costs and the time of
  each phase.
- `as_of_commit_seq` reads the graph at an older commit.
- `FILTER(?score > 0.8)` keeps only close hits.

The syntax is standard SPARQL, so existing clients send these queries
unchanged. See [Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/#search-inside-a-sparql-query).

### 3 October: MCP 0.8.1

`suggest_ontology_change` takes up to 128 operations in `change`, the same
limit as the server. MCP 0.8.0 refused more than 64.

### 3 October: `EXISTS` under aggregates and in `OPTIONAL` filters

Under an aggregate, a `BIND` with `EXISTS` left its variable unbound. This
query returned 0. Now it counts the users that are not persons:

```sparql
SELECT (SUM(?p) AS ?n) WHERE {
  ?x a ex:User
  BIND(IF(NOT EXISTS { ?x a ex:Person }, 1, 0) AS ?p)
}
```

Two other shapes returned an error. One is `EXISTS` inside a larger `FILTER`
under an aggregate. The other is `EXISTS` in the filter of an `OPTIONAL`. Both
shapes now run.

### 2 October: TypeScript and Python 0.17.0, MCP 0.8.0

TypeScript `@littlebigbrain/client` and Python `littlebigbrain` 0.17.0 add the
[ontology starters](https://docs.littlebigbrain.com/guides/ontology-starters/). `ontology.starters` lists,
applies and updates `crm`, `documents` and `work`. Typed constants give each
starter term its SPARQL IRI.

A refusal now carries the reasons for each item in `LbbError.details`. An
example is the `conflicts` of `409 starter_conflict`.

Version 0.16.0 came out the same day. It added these methods:

- `ontology.suggestions` reads and decides
  [ontology change suggestions](https://docs.littlebigbrain.com/guides/ontology-suggestions/).
- `query.sparql` takes `profile` and returns how the query ran. See
  [See how a query ran](https://docs.littlebigbrain.com/guides/query-profiles/).
- `plannerStats` returns the statistics of the query planner.
- `modelActivity` reports what each managed model did in one month.
- `workflows.deleteInstance` deletes a workflow instance and its history.

Python uses snake case names, such as `planner_stats`. Version 0.16.0 drops
`ontology.induce`, whose route the server no longer has.

The Python client now leaves unset optional fields out of a model request
body. It sent them as `null`, and the server refused some requests with
HTTP 400.

MCP 0.8.0 adds `list_starters`, `apply_starter` and `suggest_ontology_change`
to `lbb_configure`. `lbb_inspect` lists suggestions with
`action: "ontology_suggestions"`.

### 2 October: `xsd:dateTime()` casts a string

The cast `xsd:dateTime("2026-10-01T00:00:00Z")` left its variable unbound.
Now it returns `"2026-10-01T00:00:00Z"^^xsd:dateTime`, the same value as
`STRDT`. The casts `xsd:date()` and `xsd:time()` work too.

- A string casts to each of the three datatypes.
- An `xsd:dateTime` casts to `xsd:date` and `xsd:time`.
- An `xsd:date` casts to `xsd:dateTime`, at the start of that day.
- `xsd:string()` of a date or time returns its canonical form.
- An invalid value, such as month 13, is a type error. A `BIND` stays unbound.

Every cast now ignores spaces around a string, as XPath prescribes. So
`xsd:integer(" 42 ")` returns 42.

### 2 October: Entailment applies inside `EXISTS` and `NOT EXISTS`

With `entailment=subclass`, `rdfs` or `owl`, the pattern inside `EXISTS` matched
only stored statements. The main pattern and `MINUS` used the entailed graph.
Now every pattern in the query uses the entailed graph. This includes an
`EXISTS` in a `FILTER`, `BIND`, `IF`, projected expression or `HAVING`.

Here `ex:User` is a subclass of `ex:Person`. The query now returns no users,
the same answer as `MINUS { ?x a ex:Person }`:

```sparql
SELECT ?x WHERE { ?x a ex:User FILTER NOT EXISTS { ?x a ex:Person } }
```

### 2 October: A date with a very long year is stored

A date with a year past 9223372036854775807 failed the write or the query.
An example is `"99999999999999999999-01-01"^^xsd:date`. Now the write stores
the value unchanged, and a query reads it back. The value is out of range, so
a comparison or `YEAR()` treats it as a type error.

### 2 October: `GROUP BY` keeps a fixed subject or object

A `GROUP BY` over one triple pattern ignored a fixed subject or object. This
query returned each subject that has a type:

```sparql
SELECT ?x (COUNT(*) AS ?n) WHERE { ?x a ex:User } GROUP BY ?x
```

Now it returns only the members of `ex:User`. The fix also applies to
`?x ex:knows ?x`, which repeats the grouped variable. It applies to a count
sorted with `ORDER BY` and `LIMIT`, and to two joined patterns.

## September 2026

### 28 September: Search by meaning

Search by meaning is now available for every account. Declare an embedding on
a class. The platform builds one text per record from the fields you choose,
embeds it with the graph's model, and keeps the vectors current after each
published commit. A search embeds your question with the same model and
returns the nearest records. Each hit is checked against one published commit,
so a deleted or re-typed record does not come back.

- `POST /v1/embeddings/preview` shows the text of sample records and the facts
  you can choose. It stores nothing and calls no model.
- `PUT /v1/embeddings` declares an embedding and starts the embed job.
  `GET /v1/embeddings` lists the embeddings and their state, and
  `DELETE /v1/embeddings` removes one.
- `POST /v1/search` searches every searchable class. A `filter` limits the
  hits to a class and its subclasses, or to records linked to a named record.
  With `"explain": true`, the search returns its plan and calls no model.
- `PUT /v1/embeddings/model` moves every class of a graph to a new model. The
  graph switches when every class is ready, so a search never mixes models.

The TypeScript and Python SDKs add `embeddings` methods, including `search`.
The MCP server adds `lbb_embeddings`, `lbb_embeddings_manage` and
`lbb_embeddings_delete`, and `lbb_query` gets `mode=search`. Vectors count as
stored data; model tokens are not billed. See
[Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/).

### 27 September: `BIND(EXISTS …)` returns a value in a joined group

In a braced group joined with other patterns, `BIND(EXISTS { … } AS ?e)` left
`?e` unbound. Now `?e` is `true` or `false`. The `EXISTS` uses the variables
that its own group binds. `NOT EXISTS` works the same way.

```sparql
SELECT ?k ?e WHERE {
  ?k ex:memberOf ex:platform .
  { ?k a ?c BIND(EXISTS { ?k ex:knows ?o } AS ?e) }
}
```

### 26 September: `owl:sameAs` no longer makes a record a type

With `entailment=owl`, `SELECT ?t WHERE { ex:alice a ?t }` listed `ex:alice`
as a type. It also listed each `owl:sameAs` alias of `ex:alice`. The query now
lists only classes.

A `BIND` in a join could replace a value that another pattern bound. Now the
two values must be equal, or the row is dropped. In an `OPTIONAL`, a left row
with a different value keeps its value and stays unmatched.

### 26 September: JSON type hierarchies reach queries

A type's `super_types` in a JSON schema publish one `rdfs:subClassOf` statement
per parent. So does the `add_super_types` evolve operation. With
`entailment=subclass`, `rdfs` or `owl`, a query for a type also matches its
subtypes. `SELECT ?c ?p WHERE { ?c rdfs:subClassOf ?p }` lists the links.

A strong read applies a new parent at once. An eventual read applies it after
the graph republishes. Republication starts when the schema change is applied.
See
[Add a type hierarchy to JSON records](https://docs.littlebigbrain.com/guides/reasoning/#add-a-type-hierarchy-to-json-records).

### 25 September: Record-history routes removed

These read routes are removed:

- `GET /v1/graph/entity/metadata` and `GET /v1/graph/entity/neighborhood`
- `GET /v1/graph/entities/sample` and `GET /v1/graph/changes`
- `POST /v1/query/state`, `/v1/query/history`, `/v1/query/transitions`,
  `/v1/query/why` and `/v1/query/conflicts`

They read data that hosted graphs do not publish. So they always answered
`429 ingest_busy`, and a client retried them forever. They now return `404`.
The `did_you_mean` field of the error names the route to use.

Read one record with its fields and links with `GET /v1/graph/entity`. Read a
past commit with SPARQL and `as_of_commit_seq`.
[Replay answers and history](https://docs.littlebigbrain.com/guides/time-travel-audit/) shows how.

The TypeScript and Python SDKs drop the matching methods. The MCP server drops
the `state`, `history`, `transitions` and `why` actions of `lbb_inspect`.

### 24 September: One history per graph

Graphs have no branches. Each graph keeps one line of commits.

The branch fork, merge and delete routes are removed. So are the `branch`
query parameter and observe (`POST /v1/memory/observe`). The SDKs and the MCP
server drop their branch and observe methods and tools.

Items in `GET /v1/graphs` drop the `branches` field. Graph metadata drops
`parent`. The graph delete response drops `deleted_branches`.

A request with `branch=main` still works, and the server ignores the value.
Any other `branch` value returns `400 branch_not_supported`.

### 6 September: OPTIONAL keeps its columns under entailment

A SPARQL query with an OPTIONAL block, run with `entailment=owl` or
`entailment=rdfs`, returned rows missing variables bound by the required
pattern whenever the optional pattern matched an entailed triple, and those
rows collapsed into duplicates. The same happened, without entailment, for a
subquery inside OPTIONAL. Every row now carries the required pattern's
bindings, and a subquery's unprojected variables no longer take their values
from the outer row.

OPTIONAL under `entailment=owl` also read far more than it should on hosted
graphs whose ontology declares `owl:sameAs` identities: each row of the
required pattern re-read the optional pattern's blocks, so a small graph
could exhaust its per-request read budget and answer `429 ingest_busy`. The
optional side now folds once for the whole result, including an OPTIONAL over
an entailed-only property and the `OPTIONAL { ?x ?p ?y }` shape, and a query
that does exceed the read budget answers `400 scan_too_wide` instead of a
retryable backlog code.

## August 2026

### 31 August: Import typed ontology properties

Ontology imports recognize `owl:DatatypeProperty` declarations as property
fields. Their declared ranges map to native text, numeric, boolean, and temporal
types. Object properties remain relationships between classes.

TypeScript `@littlebigbrain/client` 0.13.0 and Python `littlebigbrain` 0.13.0
add typed `schemaSummary()` and `schema_summary()` methods for inspecting a
graph's schema.

### 28 August: Preserve imported class hierarchies

Additive ontology definitions preserve `rdfs:subClassOf` relationships. The
ontology and schema APIs expose direct superclass identifiers and canonical
query IRIs, including when imported vocabularies share local class names.

### 25 August: Query with OWL entailment

SPARQL accepts `entailment=owl` for supported OWL reasoning, including equivalent
classes and properties, inverse and transitive relationships, and bounded
property chains. Reasoning applies when you query published data.

See [Reasoning & inference](https://docs.littlebigbrain.com/guides/reasoning/) for the supported rules and
their limits.

### 24 August: RDFS reasoning and clearer validation

SPARQL accepts `entailment=rdfs` for subclass, subproperty, and domain/range
reasoning, or `entailment=subclass` for the class-only subset. Asserted facts
remain the default query mode.

SHACL shape publishing rejects unsupported or malformed constraints with clear
errors. SPARQL constraints support declared prefixes, validation messages can
include the offending values, and nested violations identify the constraint
that failed.

TypeScript and Python 0.12.0 support publishing RDF SHACL shapes and reading
ontology conformance reports. See [Query & validate](https://docs.littlebigbrain.com/guides/sparql-and-shacl/).

### 24 August: Clear errors for named-graph input

RDF imports, SPARQL writes, and Graph Store requests report unsupported
named-graph input. Each database graph serves one default RDF graph. Use
separate database graphs and branches for isolation, and check import error
counts or enable strict import validation.

See [HTTP & SPARQL](https://docs.littlebigbrain.com/sdks/http-api/).

### 22 August: Reusable ontology definitions and publication waiters

New graphs start with an empty ontology. Reapplying the same definition is a
no-op; additive definitions evolve the ontology while incompatible changes
return an error. The starter vocabulary can be added explicitly with
`merge_default: true`.

TypeScript and Python 0.11.1, and MCP 0.4.1, improve publication waiting with
dedicated deadlines and retries. Waiters finish when the requested data is
queryable.

### 21 August: SPARQL APIs and large RDF imports

The public query API is SPARQL, with native protocol endpoints and TypeScript,
Python, and MCP clients. TypeScript and Python 0.11.0, and MCP 0.4.0, align
with that query surface. See [HTTP & SPARQL](https://docs.littlebigbrain.com/sdks/http-api/).

Large RDF imports can defer publication while a chunked load is in progress,
then make the completed import queryable. The bulk loader supports resumable
input. See [Load RDF data](https://docs.littlebigbrain.com/guides/load-rdf/).

Selective relationship queries and multi-hop SPARQL patterns also execute
faster while preserving their results.

### 20 August: Reliable joins across RDF term orders

Join results are independent of the order in which RDF terms entered a graph.

### 19 August: Extract years and months from temporal literals

`YEAR()` accepts `xsd:gYear` and `xsd:gYearMonth`; `MONTH()` accepts
`xsd:gYearMonth`. Aggregates can use these typed values directly. An aggregate
whose inputs all produce type errors returns an unbound result.

### 18 August: Faster filters and clearer temporal comparisons

Numeric, date, and string filters avoid scanning unrelated values. Comparisons
between supported temporal values distinguish an undefined comparison from a
valid comparison that matches no rows.

See [FILTER comparisons and datatypes](https://docs.littlebigbrain.com/guides/sparql-and-shacl/#filter-comparisons-and-datatypes).

### 15 August: Faster schema summaries and statistics

Grouped statistics, property inventories, and schema summaries respond faster
on large graphs.

### 14 August: Bounded query memory

Large scans use bounded working memory, and queries report explicit errors
when they exceed their resource or result limits.

### 13 August: Faster counting and entity lookups

`COUNT`, `COUNT(DISTINCT ...)`, and queries anchored at a known entity perform
less work on large graphs.

### 9 August: Resume interrupted index updates

Interrupted index updates resume their progress. Unused index data is reclaimed
according to retention rules.

### 6 August: Incremental indexing and bounded neighborhoods

Indexes incorporate updates incrementally. Entity neighborhood reads apply
explicit limits when exploring highly connected data.

## July 2026

### 30 July: Durable large imports

Large import jobs continue across restarts and report progress. TypeScript and
Python 0.9.0 stream NDJSON request bodies to reduce client memory usage.

### 27 July: Validation as ontologies evolve

Active SHACL shapes remain attached when an ontology evolves. Common minimum
and maximum count constraints validate data on large graphs.

### 21 July: Selectable read consistency

Reads default to the published snapshot. Applications can request strong
consistency and specify the write sequence that must be visible.

See [Core concepts](https://docs.littlebigbrain.com/start/concepts/#when-a-read-sees-your-write).

### 20 July: Graph fork and reload

The HTTP API and TypeScript and Python 0.7.0 clients support forking a graph
and replacing its contents through a reload operation.

### 19 July: Faster SPARQL on large graphs

SPARQL `SELECT`, ordered queries with limits, and repeated queries execute
more efficiently on large datasets.

### 18 July: Faster graph reports

Graph summary, conformance, and metadata requests respond faster. Bounded
exploration responses report the limits they applied.

### 15 July: Automatic retries under load

Overloaded requests return `429` with `Retry-After`. TypeScript and Python
0.6.0 retry these requests within a deadline.

### 11 July: Export graph data

Graphs export as NDJSON for backup and transfer between stacks.

### 10 July: Query and validate persisted graphs

SPARQL queries and SHACL validation operate on graphs stored in object storage.
Set-valued properties support fields such as tags and categories.

### 6 July: Validated branch merges and historical reads

Branch merges validate proposed changes and report conflicts. Rejected writes
identify the violated constraint so clients can correct their input.

Applications can pin reads to a retained commit for reproducible results. See
[History & time travel](https://docs.littlebigbrain.com/guides/time-travel-audit/) for supported selectors and
retention boundaries.

## June 2026

### 30 June: Faster bulk ingest

Bulk ingest throughput improved by 3x over the previous implementation.

### 26 June: Aggregation and repeatable imports

SPARQL supports grouped `COUNT`, `SUM`, and `AVG` queries over typed values.
NDJSON imports can use stable external keys to update existing entities, and
retractions remove previously asserted facts.

### 25 June: Query class hierarchies

Subclass reasoning lets a query for a broad class include its subclasses.

### 24 June: Ontology conformance reports

Conformance reports check published data against the active ontology and are
available through the HTTP API and SDKs.

### 23 June: Additive ontology evolution

Ontologies can add entity types and widen relationship domains or ranges while
preserving existing records.

### 21 June: Typed literals

Numeric and temporal literals support server-side filtering and aggregation.

### 20 June: Bounded inference rules

Supported SHACL rules derive additional facts for querying. See
[Reasoning & inference](https://docs.littlebigbrain.com/guides/reasoning/) for configuration and limits.

### 11 June: Stable reads during writes

Graph snapshots keep query results consistent while new data arrives.
