# Replay answers and history

Source: https://docs.littlebigbrain.com/guides/time-travel-audit/

The [quickstart](https://docs.littlebigbrain.com/start/quickstart/) runs its path query at the commit the
search read. It passes `as_of_commit_seq: found.served_at_seq`, so search and
path read one version of the data.

This page shows how to store that commit with an answer and replay the query
later. It also shows how to find the commit where an answer changed.

The last sections cover pinned reads, valid time and how long history is
kept. Commits are explained in [core concepts](https://docs.littlebigbrain.com/start/concepts/#commits).

## How replay works

- Each write is a commit with a number, `commit_seq`. A later write never
  changes an earlier commit.
- Each read reports the commit it read. SPARQL text returns
  `snapshot.served_at_seq`, and search returns `served_at_seq`.
- A query with `as_of_commit_seq` reads the graph as it was at that commit.
  It returns the same rows every time while the commit is retained.
- A pinned read is exact. The platform never answers from a nearby commit
  instead.

Replay reproduces the evidence behind an answer. The generated text needs more.
To reproduce it, also store the prompt, the model version and its settings.

## 1. Store the answer with its commit

The steps on this page continue the quickstart on the same `support` graph.
Run the quickstart first.

Store these with each answer:

| Item | Why |
| --- | --- |
| The graph name | A commit number belongs to one graph. |
| The question and the search hits | Search takes no commit pin, so store its hits. |
| The query text and its options | A replay runs the same query. |
| The rows | You can compare a replay with what the agent saw. |
| The commit, `served_at_seq` | A replay reads the graph at this commit. |

Add this snippet to the end of your quickstart file and run the file once. It
writes `answer.json` on the first run only, so later runs keep the first
answer.

### TypeScript

```ts
import { existsSync, writeFileSync } from "node:fs";

if (!existsSync("answer.json")) {
  const answer = {
    graph: "support",
    question: "users cannot sign in after the upgrade",
    hits: found.hits.map((hit) => ({ iri: hit.iri, label: hit.label, score: hit.score })),
    query,
    rows,
    commit: found.served_at_seq,
  };
  writeFileSync("answer.json", JSON.stringify(answer, null, 2));
}
```

### Python

```python
if not os.path.exists("answer.json"):
    answer = {
        "graph": "support",
        "question": "users cannot sign in after the upgrade",
        "hits": [{"iri": h["iri"], "label": h["label"], "score": h["score"]} for h in found["hits"]],
        "query": query,
        "rows": rows,
        "commit": found["served_at_seq"],
    }
    with open("answer.json", "w") as f:
        json.dump(answer, f, indent=2)
```

### cURL

Run this in the directory that holds `found.json` from the quickstart. It
stores the query as `path.rq`, then the answer without its rows.

```sh
cat > path.rq <<EOF
PREFIX dr: <https://littlebigbrain.com/r/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?bug ?release WHERE {
  <$(jq -r '.hits[0].iri' found.json)> dr:reported_as ?b .
  ?b rdfs:label ?bug ; dr:fixed_in ?r .
  ?r rdfs:label ?release .
}
EOF

[ -f answer.json ] || jq --rawfile query path.rq '{
    graph: "support",
    question: "users cannot sign in after the upgrade",
    hits: [.hits[] | {iri: .iri, label: .label, score: .score}],
    query: $query,
    commit: .served_at_seq
  }' found.json > answer.json
```

On a new graph, `commit` is `1`.

## 2. Correct the data

Suppose the release notes were wrong. The fix for `AUTH-112` shipped in 3.2.1.
Two writes correct the graph. The first retracts the old `FIXED_IN` link. The
second adds the new one.

Create a new file, `correct.mts` or `correct.py`, and run it once:

### TypeScript

```ts
import { LbbClient } from "@littlebigbrain/client";

const lbb = new LbbClient({
  baseUrl: process.env.LBB_URL!,
  apiKey: process.env.LBB_API_KEY!,
  graph: "support",
});
const auth = { type: "Bug", key: "AUTH-112", name: "session token expiry" };
const r32 = { type: "Release", key: "3.2", name: "Release 3.2" };
const r321 = { type: "Release", key: "3.2.1", name: "Release 3.2.1" };

await lbb.graph("support").retract(
  { edges: [{ source: auth, relation: "FIXED_IN", target: r32 }] },
  { idempotencyKey: "fix-moved-1" },
);
const corrected = await lbb.graph("support").facts.create(
  { triplets: [{ source: auth, relation: "FIXED_IN", target: r321, evidence: "Release notes 3.2.1" }] },
  { idempotencyKey: "fix-moved-2" },
);
await lbb.waitForPublished(corrected.commit_seq);
console.log("corrected at commit", corrected.commit_seq);
```

### Python

```python
import os

from lbb import LbbClient

lbb = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support")
graph = lbb.graph("support")
auth = {"type": "Bug", "key": "AUTH-112", "name": "session token expiry"}
r32 = {"type": "Release", "key": "3.2", "name": "Release 3.2"}
r321 = {"type": "Release", "key": "3.2.1", "name": "Release 3.2.1"}

graph.retract(
    {"edges": [{"source": auth, "relation": "FIXED_IN", "target": r32}]},
    idempotency_key="fix-moved-1",
)
corrected = graph.facts.create(
    {"triplets": [{"source": auth, "relation": "FIXED_IN", "target": r321, "evidence": "Release notes 3.2.1"}]},
    idempotency_key="fix-moved-2",
)
graph.wait_for_published(corrected["commit_seq"])
print("corrected at commit", corrected["commit_seq"])
```

### cURL

```sh
AUTH='{"type": "Bug", "key": "AUTH-112", "name": "session token expiry"}'

curl --fail-with-body -sS -X POST "$LBB_URL/v1/graph/retract?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: fix-moved-1" \
  -d "{\"edges\": [{\"source\": $AUTH, \"relation\": \"FIXED_IN\",
       \"target\": {\"type\": \"Release\", \"key\": \"3.2\", \"name\": \"Release 3.2\"}}]}"

curl --fail-with-body -sS -X POST "$LBB_URL/v1/graph/commit?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: fix-moved-2" \
  -d "{\"triplets\": [{\"source\": $AUTH, \"relation\": \"FIXED_IN\",
       \"target\": {\"type\": \"Release\", \"key\": \"3.2.1\", \"name\": \"Release 3.2.1\"},
       \"evidence\": \"Release notes 3.2.1\"}]}"

until curl -sS "$LBB_URL/v1/graph/publication-status?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" | grep -q '"state": *"current"'; do sleep 1; done
```

After the quickstart, the SDK examples print `corrected at commit 3`. The
retraction is commit 2. A retraction is a new commit, and commit 1 still holds
the old link.

## 3. Replay the answer

Replay reads `answer.json` and runs the stored query twice. The first run is
pinned to the stored commit. The second reads the latest published commit.

Create `replay.mts` or `replay.py`:

### TypeScript

```ts
import { readFileSync } from "node:fs";
import { LbbClient, parseSparqlResults } from "@littlebigbrain/client";

const lbb = new LbbClient({
  baseUrl: process.env.LBB_URL!,
  apiKey: process.env.LBB_API_KEY!,
  graph: "support",
});
const answer = JSON.parse(readFileSync("answer.json", "utf8"));

const then = await lbb.sparqlText({ query: answer.query, as_of_commit_seq: answer.commit });
const now = await lbb.sparqlText({ query: answer.query });
console.log("commit", then.snapshot?.served_at_seq, parseSparqlResults(then).rows);
console.log("commit", now.snapshot?.served_at_seq, parseSparqlResults(now).rows);
```

### Python

```python
import json
import os

from lbb import LbbClient, SparqlResults

lbb = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support")
with open("answer.json") as f:
    answer = json.load(f)

def run(body):
    resp = lbb.raw_request("POST", "/v1/query/sparql-text", params={"graph": answer["graph"]}, body=body)
    return resp.data["snapshot"]["served_at_seq"], SparqlResults.from_envelope(resp.data).rows()

print("commit", *run({"query": answer["query"], "as_of_commit_seq": answer["commit"]}))
print("commit", *run({"query": answer["query"]}))
```

### cURL

```sh
for body in "$(jq '{query, as_of_commit_seq: .commit}' answer.json)" "$(jq '{query}' answer.json)"; do
  curl --fail-with-body -sS -X POST "$LBB_URL/v1/query/sparql-text?graph=support" \
    -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \
    -d "$body" \
    | jq -c '{commit: .snapshot.served_at_seq, rows: [.results | fromjson | .results.bindings[] | {bug: .bug.value, release: .release.value}]}'
done
```

The TypeScript example prints this:

```text
commit 1 [ { bug: 'session token expiry', release: 'Release 3.2' } ]
commit 3 [ { bug: 'session token expiry', release: 'Release 3.2.1' } ]
```

The replay still returns Release 3.2, the answer the agent gave. The latest
data says Release 3.2.1. Both results name their commit.

The Python SDK's `sparql()` method takes no `as_of_commit_seq`. Use
`raw_request` for a pinned read, as above.

## 4. Find the commit where the answer changed

Run the stored query at each commit from the stored one to the latest. Add this
to the end of `replay.mts` or `replay.py`:

### TypeScript

```ts
const { published_seq } = await lbb.publicationStatus();
for (let seq = answer.commit; seq <= published_seq; seq++) {
  const res = await lbb.sparqlText({ query: answer.query, as_of_commit_seq: seq });
  const releases = parseSparqlResults(res).rows.map((row) => row.release);
  console.log(seq, releases.join(", ") || "no rows");
}
```

### Python

```python
published = lbb.publication_status()["published_seq"]
for seq in range(answer["commit"], published + 1):
    _, rows = run({"query": answer["query"], "as_of_commit_seq": seq})
    print(seq, ", ".join(row["release"] for row in rows) or "no rows")
```

### cURL

```sh
LATEST=$(curl -sS "$LBB_URL/v1/graph/publication-status?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY" | jq '.published_seq')

for seq in $(seq "$(jq '.commit' answer.json)" "$LATEST"); do
  printf '%s ' "$seq"
  curl --fail-with-body -sS -X POST "$LBB_URL/v1/query/sparql-text?graph=support" \
    -H "Authorization: Bearer $LBB_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq --argjson seq "$seq" '{query, as_of_commit_seq: $seq}' answer.json)" \
    | jq -r '[.results | fromjson | .results.bindings[].release.value] | join(", ")'
done
```

Each example prints one line per commit:

```text
1 Release 3.2
2 no rows
3 Release 3.2.1
```

Commit 2 removed the old link, and commit 3 added the new one. cURL prints
nothing after the `2`.

This runs one query per commit. For a long history, compare the stored commit
with the latest first. Then halve the range until you find the commit.

## Where a commit pin works

| Surface | How to pin | Notes |
| --- | --- | --- |
| `POST /v1/query/sparql-text` | `as_of_commit_seq` in the JSON body | TypeScript: `sparqlText({ query, as_of_commit_seq })`. Python: `raw_request`. |
| `GET` or `POST /sparql` | `?as_of_commit_seq=` | For standard SPARQL tools. |
| `POST /v1/query/sparql` | `as_of_commit_seq` in the JSON body | The structured JSON query. |
| `GET /v1/graph/entity` | `?as_of_commit_seq=` | One JSON record with its links. TypeScript: `entityDetail({ type, key, asOfCommitSeq })`. |
| `POST /v1/search` | No pin | Store the hits with the answer. |

A pin works the same on a graph loaded from RDF. `GET /v1/graph/entity` reads
JSON records only. On an RDF-native graph, query the subject IRI with SPARQL
instead.

A pinned response describes the commit in `snapshot`:

| Field | Meaning |
| --- | --- |
| `as_of_commit_seq` | The pin you sent. |
| `served_at_seq` | The commit the read used. It equals the pin. |
| `commit_seq` | The newest commit of the graph. |
| `stale`, `stale_reason` | `true` and `eventual_consistency` when the pin is older than the newest commit. This is expected for a replay. |

These requests fail:

| Request | Response |
| --- | --- |
| A pin above the newest commit | `400 invalid_request`, `as-of commit 9 is beyond graph head 3` |
| `min_indexed_seq` above the pin | `400 invalid_request` |
| A commit that is not retained | `400 historical_generation_unavailable`. Retrying does not help. |
| `/sparql?as_of_valid_time=` | `400 historical_generation_unavailable`. See [Valid time](#valid-time). |

## Record the validation state

If the graph has SHACL shapes, store the validation state with the answer.
`GET /v1/ontology/conformance` returns the report of the latest validated
commit:

```sh
curl --fail-with-body -sS "$LBB_URL/v1/ontology/conformance?graph=support" \
  -H "Authorization: Bearer $LBB_API_KEY"
```

Store `conforms`, `result_count`, `validated_at_seq` and `ontology_version`.
Store `shapes_version` too when the graph has published shapes.

The report covers the commit in `validated_at_seq`. A request never validates
a past commit. With `consistency=strong`, the route returns a retryable
`429 strong_read_pending` until the report covers the newest commit.

## How long history is kept

A pinned read works while the commit is retained. A commit that is no longer
retained fails with `historical_generation_unavailable`. The read never
returns a nearby commit instead. Deleting the graph deletes its history.

Keep your own copy of evidence that must outlive the graph. That is the
question, the query, the rows and the commit. The `answer.json` from step 1
is such a copy.

## Valid time

A commit pin answers one question: what did the graph record at commit N? A
second question is when a fact was true in the world. That is valid time.

A link can carry `valid_time` with a `start` and an `end`:

```json
{
  "source": {"type": "Bug", "key": "AUTH-112", "name": "session token expiry"},
  "relation": "FIXED_IN",
  "target": {"type": "Release", "key": "3.2", "name": "Release 3.2"},
  "valid_time": {"start": "2026-09-02T00:00:00Z"}
}
```

Hosted graphs store `valid_time` but do not query by it. SPARQL and the record
read take commit pins only.

To ask what was true on a date, store the dates as `date_time` fields. Then
filter on them in SPARQL. [Build with JSON records](https://docs.littlebigbrain.com/guides/without-rdf/)
shows a `date_time` field.

## Related

- [Quickstart](https://docs.littlebigbrain.com/start/quickstart/): the search and path query this page
  replays.
- [Load RDF data](https://docs.littlebigbrain.com/guides/load-rdf/): pins work the same on RDF data.
- [Query and validate](https://docs.littlebigbrain.com/guides/sparql-and-shacl/): SPARQL, SHACL shapes and
  the conformance report.
- [Reasoning & inference](https://docs.littlebigbrain.com/guides/reasoning/): combine `entailment` with a
  commit pin.
- [HTTP API](https://docs.littlebigbrain.com/sdks/http-api/): every route and parameter.
