Idempotency
Two mechanisms keep writes safe to repeat — request_id idempotency (same id, same response) and content dedup (BLAKE3 fingerprint, was_deduplicated).
- A Brain client.
- Familiarity with Adding memories.
- An at-least-once source (Kafka, SQS, webhook delivery) that may redeliver.
A queue worker that ingests messages exactly-once into Brain by reusing one request id per logical message across retries, and understands when a repeat produces was_deduplicated: true versus a cached response.
Two mechanisms, two jobs
They are easy to confuse but do different things:
Request-id idempotency protects against retries (the same call sent twice). Content dedup protects against re-ingestion (the same text arriving through different calls). A retry may hit either: the cached response if the id repeats, or was_deduplicated if the id is fresh but the text already exists.
Step-by-step
The wire builders mint a fresh requestId on build(). To make a retry idempotent, build the request once and resend the same object — the id is stable, so the server dedupes the retry to one logical write.
import { EncodeBuilder } from "@brain-db/sdk";
// Build once — the requestId is minted here. Resend the SAME object on a
// retry and the server dedupes it to one logical write.
const req = new EncodeBuilder(msg.text).context(msg.topicId).build();
let res;
for (let attempt = 0; attempt < 5; attempt++) {
try { res = await client.encode(req); break; }
catch (e) { if (attempt === 4) throw e; }
}from brain_db_sdk import EncodeBuilder, with_retry
# Build once — the request_id is minted here and reused on every attempt.
req = EncodeBuilder(msg.text).context(msg.topic_id).build()
res = with_retry(lambda: client.encode(req))use brain_db_sdk::EncodeBuilder;
// Build once — the request_id is minted here. Resend the SAME object on a
// retry and the server dedupes it to one logical write.
let req = EncodeBuilder::new(&msg.text).context(msg.topic_id).build();
let mut res = None;
for attempt in 0..5 {
match client.encode(&req).await {
Ok(r) => { res = Some(r); break; }
Err(e) => if attempt == 4 { return Err(e.into()); },
}
}Do not rebuild the request inside the retry loop — a new build() mints a new id and defeats the mechanism. Same id + same params → the cached response; same id + different params → a Conflict.
The HTTP encode uses a stable server-side request id and is retried automatically on 503 and transport failures per the client's retry policy — so a lost response on the wire is safe. Other HTTP verbs run exactly once. See Retries and cancellation.
const brain = new BrainHttpClient({
apiKey: process.env.BRAIN_API_KEY!,
retry: { maxAttempts: 5 },
});
const res = await brain.encode({ text: msg.text, context: msg.topicId });from brain_db_sdk.http.retry import HttpRetryPolicy
brain = BrainHttpClient(
os.environ["BRAIN_API_KEY"], retry=HttpRetryPolicy(max_attempts=5),
)
res = brain.encode(text=msg.text, context=msg.topic_id)use std::time::Duration;
use brain_db_sdk::{BrainHttpClient, HttpRetryPolicy};
use brain_db_sdk::http::EncodeInput;
let brain = BrainHttpClient::new("https://api.arc-labs.ai", &std::env::var("BRAIN_API_KEY")?)
.with_retry_policy(HttpRetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(2)));
let res = brain
.encode(&EncodeInput {
text: msg.text.clone(),
context: Some(msg.topic_id),
..Default::default()
})
.await?;When the same text is submitted again (a replayed message, a re-summarized document), content dedup returns the existing memory instead of creating a duplicate. The fingerprint is BLAKE3(text) scoped to (agent, context).
const a = await brain.encode({ text: "User upgraded to Pro." });
// … message redelivered …
const b = await brain.encode({ text: "User upgraded to Pro." });
console.log(a.memory_id === b.memory_id); // true
console.log(b.was_deduplicated); // true — no new row was createdSometimes identical text is a genuinely distinct observation (the same status re-stated at a different time). Opt out of content dedup with allowDuplicates on the wire client, ideally paired with a distinct occurredAt.
await client.encode(
new EncodeBuilder("Deploy succeeded.")
.occurredAt(BigInt(Date.now()) * 1_000_000n)
.allowDuplicates()
.build(),
);End-to-end: at-least-once into exactly-once
Derive a stable context (or request id) from the upstream identifier so retries collapse to one write, and let content dedup catch any that slip through with a fresh id.
async function ingest(msg: KafkaMessage) {
// One requestId per message; a retry resends this same object and dedupes
// server-side. Any that slip through with a fresh id hit content dedup.
const req = new EncodeBuilder(msg.value.toString())
.context(BigInt(msg.partition))
.build();
await client.encode(req);
}def ingest(msg):
req = EncodeBuilder(msg.value().decode()).context(msg.partition()).build()
# One request_id per message; retries reuse it and dedupe server-side.
with_retry(lambda: client.encode(req))use brain_db_sdk::{with_retry, BrainClient, EncodeBuilder, RetryPolicy};
async fn ingest(client: &BrainClient, msg: &KafkaMessage) -> brain_db_sdk::Result<()> {
// One request_id per message; a retry resends this same object and dedupes
// server-side. Any that slip through with a fresh id hit content dedup.
let req = EncodeBuilder::new(&msg.value).context(msg.partition).build();
with_retry(&RetryPolicy::default(), || client.encode(&req)).await?;
Ok(())
}Don't rebuild the request between attempts, and don't mutate the text to "force" a write past dedup — reach for allowDuplicates (with a distinct occurredAt) when repeats are legitimate. Mutating text to dodge the fingerprint corrupts your own data.
Was this page helpful?
Streaming
Real streaming lives on the wire client — the *Frames methods drain a verb's response frame-by-frame with cursors, cumulative counts, and isFinal. The HTTP client has none.
Error handling
Catch the right error per client — BrainHttpError for HTTP, the BrainError hierarchy for the wire — decide retry vs surface, and know which of the nine categories are transient.