Brainby arc-labs/docs
TypeScript SDK

Retries

Retry policies for both clients — the HTTP client's built-in policy for idempotent verbs, the wire client's withRetry combinator, and how deadlines cancel a call.

HTTP client

BrainHttpClient retries only the idempotent verbsencode, whoami, capabilities — and only when the failure is a transport error (status === 0) or an HTTP 503. Every other status, including all 4xx, is terminal. Backoff grows exponentially from baseDelayMs, capped at maxDelayMs; a server Retry-After header overrides the schedule.

interface HttpRetryPolicy {
  maxAttempts: number; // total attempts including the first; 1 disables retry
  baseDelayMs: number; // first backoff; doubles each attempt
  maxDelayMs: number;  // cap on any single backoff (also caps Retry-After)
}

Configure it at construction:

import { BrainHttpClient, DEFAULT_HTTP_RETRY, NO_HTTP_RETRY } from "@brain-db/sdk";

// DEFAULT_HTTP_RETRY = { maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 2000 }
const brain = new BrainHttpClient({
  apiKey: process.env.BRAIN_API_KEY!,
  retry: { maxAttempts: 5 }, // merged over the default
});

// Opt out entirely:
const once = new BrainHttpClient({ apiKey: "…", retry: NO_HTTP_RETRY });

recall, forget, link, unlink, plan, and reason are not retried by the HTTP client — they make a single attempt. Retry them yourself only when you know the operation is safe to repeat.

Wire client — withRetry

The wire client does not bake retry into each verb. Wrap a call in withRetry and it re-runs while the error is retryable (isRetryable) and attempts remain. Because each request builder mints a stable requestId, re-sending the same request is idempotent server-side (24h idempotency window), so a retried verb does not double-apply.

import { withRetry, DEFAULT_RETRY_POLICY, NO_RETRY } from "@brain-db/sdk";

// DEFAULT_RETRY_POLICY = { maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 5000 }
const req = new ForgetBuilder(memoryId).build(); // build once → stable requestId
const res = await withRetry(() => client.forget(req), DEFAULT_RETRY_POLICY);

Pass a thunk that reuses the same request object (() => client.forget(req)), not one that rebuilds it. Rebuilding mints a fresh requestId on each try, which defeats server-side idempotency.

The backoff honors a server-supplied retryAfterMs when present, otherwise grows exponentially from baseDelayMs (capped at maxDelayMs), then applies full jitter so a herd of clients that failed together don't retry in lockstep.

A transport drop is reported retryable, but the SDK does not transparently reconnect a dead socket — a retry over a closed connection re-fails fast. Recover by opening a fresh BrainClient.connect(...).

Cancellation and deadlines

Neither client takes an external AbortSignal; a call is bounded by its configured deadline instead.

Each request is bounded by timeoutMs (default 30_000), enforced with an internal AbortController. On expiry the request is aborted and surfaces as a BrainHttpError with status: 0.

const brain = new BrainHttpClient({ apiKey: "…", timeoutMs: 5_000 });

connectTimeoutMs bounds the TCP connect and requestTimeoutMs bounds each response read (defaults 10_000 / 30_000). A missed deadline rejects with BrainTimeout (carrying timeoutMs), which isRetryable treats as retryable.

const client = await BrainClient.connect("127.0.0.1", 9090, {
  auth: { kind: "token", token: tokenBytes },
  requestTimeoutMs: 5_000,
});

Was this page helpful?

On this page