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
recall.correct(
id: MemoryId,
correction: string | { content: string; reason?: string },
options?: TransportRequestOptions,
): Promise<CorrectResult>;Inputs
idMemoryIdrequiredThe memory being corrected. After this call returns, this ID is the superseded memory — its supersededBy field points at the new memory.
correctionstring | CorrectionInputrequiredA bare string is shorthand for { content: <string> }. Pass an object to include reason for the audit log.
CorrectionInput:
contentstringrequiredreasonstringoptionalReturns
supersededIdMemoryIdalwaysid you passed in. Echoed back for completeness — useful when correcting in a loop.newMemoryIdMemoryIdalwaystraceIdstringalwayslatencyMsnumberalwaysWire 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_newSearches 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_newerThe 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 signal | correct | patch |
| Original row | preserved (superseded) | mutated in place |
New MemoryId | yes | no |
| Search behaviour | old hidden, new returned | unchanged ID, new content |
| Use for | semantic updates | typo 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
NOT_FOUND404fatalMEMORY_ALREADY_SUPERSEDED409fatalMEMORY_DELETED409fatalREQUEST_IN_FLIGHT409retriableretryAfterMs and submit a fresh request.PIPELINE_ERROR422fatalUNKNOWN429retriableWhen 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 dedupesCommon 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?