Brainby arc-labs/docs
Sdk typescriptCorrect & feedback
TypeScript SDK

Correct & feedback

Reference for recall.correct() — how it supersedes a memory, what gets persisted to the audit log, and how it differs from patch.

Signature

POST/v1/feedbackAPI keystable
recall.correct(
  id: MemoryId,
  correction: string | { content: string; reason?: string },
  options?: TransportRequestOptions,
): Promise<CorrectResult>;

Inputs

ParameterTypeRequired
idMemoryIdrequired

The memory being corrected. After this call returns, this ID is the superseded memory — its supersededBy field points at the new memory.

correctionstring | CorrectionInputrequired

A bare string is shorthand for { content: <string> }. Pass an object to include reason for the audit log.

CorrectionInput:

ParameterTypeRequired
contentstringrequired
The replacement content.
reasonstringoptional
Optional human-readable reason. Persisted verbatim to the audit log; surfaces in compliance exports and the dashboard's history view.

Returns

FieldTypePresence
supersededIdMemoryIdalways
The original id you passed in. Echoed back for completeness — useful when correcting in a loop.
newMemoryIdMemoryIdalways
The new memory ID carrying the corrected content. This is the ID future searches will return.
traceIdstringalways
Server-side trace ID — links to the audit-log entry.
latencyMsnumberalways
Wall-clock latency.

Wire format

recall.correct() posts to POST /v1/feedback, which is the general-purpose "tell us about this memory" endpoint on the server. The SDK fills in the right signal:

{
  "memoryId": "mem_…",
  "signal": "correct",
  "replacement": "user lives in Hamburg",
  "reason": "updated via settings page"
}

The server creates a new memory row, sets old.supersededBy = new.id, and writes both the signal and reason to the audit log. The trace ID returned points at this composite operation, not at the new memory's own creation trace.

Examples

Bare string

import { type MemoryId } from '@arc-labs/recall';

const id = 'mem_abc' as MemoryId;
const { newMemoryId } = await recall.correct(id, 'user actually lives in Hamburg');

With audit reason

const result = await recall.correct(memoryId, {
  content: 'user lives in Hamburg',
  reason: 'updated via settings page',
});

console.log(`superseded ${result.supersededId} → ${result.newMemoryId}`);

Catching the conflict states

If the memory has already been superseded by an earlier correction (or hard-deleted), the server returns 409 with the appropriate code. The SDK surfaces it as RecallConflictError:

import { RecallConflictError } from '@arc-labs/recall';

try {
  await recall.correct(id, 'new content');
} catch (err) {
  if (err instanceof RecallConflictError) {
    if (err.code === 'MEMORY_ALREADY_SUPERSEDED') {
      // Walk the chain forward and correct the latest version instead
    } else if (err.code === 'MEMORY_DELETED') {
      // The memory is gone — write a new one with recall.remember
    }
  }
}

RecallConflictError carries code for branching and retryAfterMs for the one retriable conflict (REQUEST_IN_FLIGHT). The conflict variants here are not retriable.

The supersession chain

After correct(), the memory graph looks like:

mem_old  -- supersededBy --> mem_new

Searches return only mem_new. The old memory is still queryable directly (recall.explain(mem_old) returns it with provenance.supersededBy = mem_new) but the read pipeline filters it out of result sets so you never see both.

If you correct mem_new again, you get a chain:

mem_old → mem_new → mem_newer

The chain is walked transitively. recall.explain(mem_old).provenance.supersededBy returns the immediate successor (mem_new), not the head of the chain. To walk to the latest, follow the pointer until supersededBy is undefined.

Differences from recall.memory.patch()

recall.correct()recall.memory.patch()
Audit signalcorrectpatch
Original rowpreserved (superseded)mutated in place
New MemoryIdyesno
Search behaviourold hidden, new returnedunchanged ID, new content
Use forsemantic updatestypo fixes, tag additions, confidence revisions

If a user updates their address in your settings page, that's a correct() — the meaning changed. If you discover you wrote the wrong tag on a memory at extraction time, that's a patch() — the data was always wrong, no semantic update happened.

Errors

CodeStatusRetry
NOT_FOUND404fatal
Memory ID does not exist.
MEMORY_ALREADY_SUPERSEDED409fatal
The memory has already been superseded by another correction.
MEMORY_DELETED409fatal
The memory has been deleted (soft or hard).
REQUEST_IN_FLIGHT409retriable
An idempotency-keyed correction with the same key is still being processed. Wait retryAfterMs and submit a fresh request.
PIPELINE_ERROR422fatal
Pipeline rejected the new content (e.g. junk filter, groundedness fail).
UNKNOWN429retriable
Rate limit.

When to use

Reach for correct() whenever the semantic meaning of a memory changes. The supersession chain is exactly what you want for audit trails, compliance exports, and "show me the history of this preference" UIs.

If you find yourself calling correct() in a loop to update many memories at once, consider whether the underlying signal is a single high-level event (e.g. "user changed currency") that should be captured as one recall.remember() call instead — extraction may produce the right new memories without you having to enumerate them.

Idempotency

correct() runs through the same idempotency middleware as remember(). The SDK auto-generates an Idempotency-Key header per call (UUID v4). Override it via options.idempotencyKey for retries that should converge:

const idem = crypto.randomUUID();
await recall.correct(id, { content }, { idempotencyKey: idem });
// safe to retry the exact same call with the same key — the server dedupes

Common pitfalls

The new memory inherits the type and tags of the old one — you can't change a preference into a fact via correction. If you genuinely need a different type, use recall.forget({ id }) followed by recall.remember() with new content.

reason is logged verbatim. Do not include PII you don't want in the audit log.

The returned newMemoryId is the canonical handle going forward. Update any cached pointers in your application — a UI that holds onto the old ID will get a stale view on the next recall.explain() (it will see supersededBy).

Was this page helpful?

On this page