Skip to content

Error Envelope & Hint Codes

Every argument-validation rejection engram produces, plus the one rejection that is not about an input at all (a response too large to return) — on both the MCP tool-call lane and the Connect RPC lane — carries the same structured envelope: the field(s) that failed (or the fixed pseudo-field response), a machine-stable hint code naming why, and a human-readable detail. This page is the complete, checked-off reference for that vocabulary.

field=<name> hint=<code>: <human text>

field and hint are the machine-readable parts — parse and branch on those. The text after the colon is for a human, not a contract; it has already changed wording once in this release and will again.

Single-field example — an oversized summary on store_memory:

field=summary hint=too_long: summary must be at most 512 bytes (got 700)

The same shape covers every capped field, carrying only sizes — never the submitted value. An oversized content on store_memory/schedule_memory/update_memory:

field=content hint=too_long: content too large: 70000 bytes (max 65536)

Too many tags on the same write paths:

field=tags hint=too_many: too many tags: 129 (max 128)

No new hint code was added for either of these — both reuse too_long/too_many.

Relational example — fields that cannot be combined, on list_memory:

field=cursor_mode,offset,page_token hint=mutually_exclusive: cursor_mode, offset, and page_token are mutually exclusive

field is a comma-joined list rather than a single string specifically so a relational rejection like this one has a home in the same envelope — every field the constraint relates is named, never just one arbitrarily picked.

MCP carries this string as the entire wire payload. The protocol has no structured-error slot for a tool-call rejection: the MCP SDK discards any structured result on a non-nil error and returns only the error string as text. A client on the MCP lane parses this prefix out of that string; there is no side channel.

Connect carries the same string, plus a typed error code (see Connect code mapping below) — a Connect client can branch on the code alone and treat the string as detail, or parse the same field=/hint= prefix for parity with the MCP lane.

supersede_memory accepts a set of one or more targets to merge into a single correcting record. An invalid target set rejects the whole call — a merge never partially applies to a valid subset of the set you sent.

A rejection carries exactly one failure class per response, in a fixed order evaluated top to bottom:

  1. Set shape — the supersedes array is empty, or one of its entries is blank.
  2. Addressability and access — a target you do not own, a target that does not exist, and a target whose short id matches more than one record are all the SAME rejection. Do not read a not-found response as proof an id is unused, and do not read it as proof a short id is unique — either could be why you got it. If a short id will not resolve, name that target by its full UUID instead.
  3. Rule target — one or more targets is a store_rule record, which cannot be superseded.
  4. Already superseded — one or more targets already carries a superseded_by link; supersede the current head of that chain instead.

Every offending target of the failing class is named in the same response, comma-separated, in the order you supplied them — never just the first one. Each target is echoed exactly as you wrote it, so a target you sent as a short id comes back as that short id, never as a resolved UUID.

Worked examples, taken verbatim from what the server renders — an automated test (TestSupersedeDocsMatchShippedContract) binds this text to the production rendering helper, so an out-of-date example fails the build rather than silently drifting:

Addressability and access, two offending targets:

not found: a1b2c3d4e5, f6g7h8j9k0

Already superseded, two offending targets:

target is already superseded: a1b2c3d4e5, m1n2p3q4r5

These four rejections are sentinel-shaped, not field-and-hint shaped — they name offending targets, not a field, and the twelve-code hint vocabulary above is unchanged; no new hint code was added for this verb. Set-shape rejections (empty array, blank entry) DO use the field-and-hint grammar above, naming the supersedes argument itself rather than any target value.

Batch outcomes (archive_memory / restore_memory)

Section titled “Batch outcomes (archive_memory / restore_memory)”

archive_memory and restore_memory take a batch of ids and return one outcome per id — a per-id not_found (not yours, or does not exist) is an outcome in the result, not an error, and the call still succeeds. The whole call rejects, with the ordinary field-and-hint grammar above naming ids, only on a malformed batch:

  • hint=required — ids is empty, or one of its entries is blank.
  • hint=out_of_range — ids has more than 1000 entries.
  • hint=too_long — one entry exceeds 256 bytes.

Transcribed directly from internal/server/argerror.go’s HintCode constants and checked off one by one against that file — this table cannot list a code the server does not emit.

Hint code Meaning What to do
required The field was absent entirely. Supply it — it was missing, not malformed.
conditional_required The field is required only given another field’s value or the call’s shape (e.g. a caller-authored summary being addressed on update_memory). Supply the field, given the condition named in the detail text.
too_long The field exceeds a maximum length/byte bound. Shorten the field’s value — do not resend the whole record; only this field failed.
too_many A collection field (e.g. citations, tags) exceeds a maximum count. Trim the collection to the stated bound.
enum The value is not one of the accepted set. Resend with one of the accepted values named in the detail text.
format The value fails a structural check (e.g. an RFC3339 timestamp). Correct the value’s shape; the constraint is named in the detail text.
prefix The value must start with a required prefix (e.g. a discovery scope must start with discovery:). Prepend the required prefix.
ordering A before/after or numeric ordering constraint is violated — usually between two fields, but sometimes between one field and a fixed reference such as the current time. Adjust so the stated ordering holds. Read field=: it lists every field involved, which may be one or two.
mutually_exclusive Two or more fields cannot be combined at once. Drop all but one — every field the constraint relates is listed under field=.
not_applicable The field does not apply given another field’s value on this call. Omit the field entirely rather than sending an empty or default value.
out_of_range A numeric field exceeds its documented maximum. Resend at or below the maximum named in the detail text — the value is rejected, never clamped.
response_too_large The result the request would produce exceeds what one response can carry — not a rejected input; field= is always the fixed pseudo-field response. Retry with a smaller limit or k, or omit full; retrying the identical request fails the same way. See Response too large.

out_of_range names a NUMERIC argument above its documented ceiling — distinct from too_long (a length/byte bound on a string or blob) and too_many (a collection count bound). limit/k above the documented maximum (1000) is the first caller of this code.

out_of_range reads like it should map to CodeOutOfRange below, but it is classified Malformed by decision — a rejected numeric argument is still a malformed request, and Malformed and Out of range already collapse to the same CLI exit, so nothing observable changes for a CLI-driven caller:

Hint code Connect code CLI exit
out_of_range invalid_argument (CodeInvalidArgument) 2

too_long and response_too_large are easy to conflate but name opposite directions: too_long means an INPUT field you sent exceeded a bound — shorten that field and resend. response_too_large means the RESPONSE your request would produce exceeds a bound — the request itself was fine; ask for less of it (a smaller limit/k, or without full).

required and conditional_required are two different codes for a reason: required means the field is unconditionally missing; conditional_required means it is missing given something else about the call, so retrying with the same fields you already sent minus the addition will fail again for the same reason.

mutually_exclusive always names two or more fields, never one — do not retry by guessing which single field is “the” problem field; the constraint is between all of them. list_memory’s paging trio (cursor_mode, offset, page_token) is the one three-field case today.

ordering names one or two fields, so read field= rather than assuming a pair. Two fields means they are misordered relative to each other (not_before must precede not_after). One field means it is misordered relative to a fixed reference rather than to another argument you sent — not_after must be in the future, for example, which no change to a second field can fix.

Operator-tier hint codes (engram migrate revert)

Section titled “Operator-tier hint codes (engram migrate revert)”

These two codes use the same field=<name> hint=<code>: <text> grammar as the twelve-code table above, but they are produced by internal/store/revert.go’s RevertRefusalError — not by internal/server/argerror.go — so they are not HintCode constants and the twelve-code table above remains exactly what it claims to be: a transcription of argerror.go. They surface only from an engram migrate revert refusal (see below for the two places that can happen), never from any memory-tool call.

Hint code Meaning What to do
irreversible The requested revert range includes at least one step declared irreversible — the whole operation is refused before any write. Recover via a collection snapshot taken before the forward migration ran; there is no partial-revert path around an irreversible step.
unsupported At least one record in the above-target range is stamped at a version with no reachable reverse chain to the target. Recover via a collection snapshot, or narrow the target to a version every record can actually reach.

A single refusal never carries both hint codes. If a range is both irreversible and carries an unsupported version, RevertRefusalError still emits exactly one field=/hint= envelope (the one-envelope-per-rejection contract above) — it leads with field=steps hint=irreversible (irreversible outranks unsupported: it cannot be resolved by migrating forward again, unlike an unsupported-version gap) and folds the unsupported detail into that same envelope’s text as an additional clause.

Both codes can also surface from a narrower, single-record refusal discovered inside migrate revert --apply’s write-convergence loop, after its own whole-range preflight has already passed — not only from that preflight itself. A concurrent engram migrate --apply can land a new above-target record in the window between the preflight and the write loop’s first read (or between any two write-loop passes, since the loop re-derives its backlog on every pass), and that record can turn out to be irreversible or unsupported even though the preflight never saw it. This surfaces with the identical field=/hint= grammar and the identical recovery guidance — the underlying condition (a genuinely irreversible step, or a record with no reachable chain) carries the same remedy whether discovered before the first write or between two writes — but reports Candidates: 1 for the one record that triggered it, not the whole range’s count. errors.As still recovers the same *RevertRefusedError either way, so a caller does not need to distinguish the two cases.

One consequence is worth acting on: unlike a preflight refusal, a mid-loop refusal can follow writes that already landed. migrate revert --apply reverts in batches, so a run can revert whole batches of earlier records and only then hit the racing record that trips the refusal. The refusal report therefore carries the real progress counters — reverted, failed, passes, and backlog — alongside applied: false, and the text summary says so explicitly. Treat a refusal with a non-zero reverted as a partially reverted collection: run engram migrate status to see the resulting version distribution before deciding whether to re-run the revert, migrate forward again, or restore the snapshot. A preflight refusal reports all-zero counters and needs no reconciliation.

Every argument-validation failure belongs to one of three classes, and the class — never message text — selects the Connect error code:

Class Connect code Meaning
Malformed CodeInvalidArgument Wrong shape or value: absent, unparseable, not in an enum, wrong prefix.
Out of range CodeOutOfRange Right shape, wrong magnitude: a length or numeric bound violated.
Precondition CodeFailedPrecondition A relationship or state constraint between two individually-valid fields, not a single value.

All three map to the CLI’s exitUsage (exit code 2). cmd/engram/client_common.go’s exitCodeForConnectErr already groups CodeInvalidArgument, CodeOutOfRange, and CodeFailedPrecondition under the same exit code — see the CLI guide’s exit-code table — so a Connect client driving the engram CLI needs no change. A Connect client branching on the error code directly (not through the CLI) does need to widen from CodeInvalidArgument alone to all three.

Response too large: resource_exhausted and exit 10

Section titled “Response too large: resource_exhausted and exit 10”

Every other rejection on this page is about an input: something you sent was wrong. This one is not. The request was fine — the RESULT it would produce exceeds what one response can carry. It uses the same field=<name> hint=<code>: <detail> grammar as every other rejection above, but with the fixed pseudo-field response: the response overflowed, not an argument you supplied, so there is no caller-supplied field to name.

It is not one of the three argument classes in the mapping above — a Qdrant read that overflows the client’s receive limit is a transport-layer event, not a malformed, out of range, or preconditioned argument — so it does not map to exit 2.

Hint code Connect code CLI exit
response_too_large resource_exhausted (CodeResourceExhausted, HTTP 429) 10

On the MCP lane the same envelope is the tool result’s text content, with IsError true.

field=response hint=response_too_large: the result is too large to return in one response; retry with a smaller limit or k, or omit full

The remedy is a smaller limit or k, or omitting full — retrying the identical request fails the same way, since the ceiling that tripped it does not change between requests.

The one exit code with no hint-code or Connect-code counterpart

Section titled “The one exit code with no hint-code or Connect-code counterpart”

Every exit code the CLI’s taxonomy publishes elsewhere is reachable through this page’s hint-code or Connect-code vocabulary above — except one:

Exit code Meaning
7 Findings reported under an explicit opt-in flag (e.g. spine-review verify --fail-on) — the command itself succeeded; see the CLI guide’s exit-code table.

No argument-validation hint and no Connect error code ever produces exit 7, because it never represents an invalid request — the request succeeded and reported real findings a CI step asked to be told about. It is documented here specifically because a reader auditing this page for “every way engram can fail” would otherwise miss the one exit path that isn’t a failure at all.

No value echo, with one deliberate, bounded exception. An engram rejection names the field that failed and states the constraint it violated — it never echoes the value you sent. A derived, bounded number (a byte count, an element count) may appear, but never the caller-supplied string or structure itself. This means an engram rejection is safe to log verbatim: it cannot carry a secret or an oversized blob back out through your own logs.

The one exception is supersede_memory’s multi-target rejections (sentinel-shaped, classes 2-4 above): each offending target is echoed back exactly as you wrote it — a UUID or short id you already possess, not a secret or free-form content, and individually length-bounded (see the MCP Tools reference for the per-entry cap) so the echo itself can never carry an oversized or arbitrary blob. Every other field in this grammar, including supersede_memory’s own set-shape (class 1) rejection, still names the field alone and never echoes its value.

The response_too_large envelope carries no number at all. No byte ceiling, no observed size, and no upstream transport text — those are logged server-side only; the wire text is the fixed, generic detail shown in Response too large.

The MCP 401 auth body is a separate, unchanged contract. A bearer-token rejection (missing or invalid credential) is produced by the MCP SDK’s own auth middleware, before any engram argument validation runs, and does not use this grammar. It is byte-identical to its pre-existing shape and is not covered here.

The go-sdk’s own schema-level rejections are also outside this grammar. Required-ness for engram’s arguments now lives entirely in engram’s own validation (see MCP Tools reference), but the MCP SDK can still reject a call for a reason upstream of engram entirely — a malformed JSON-RPC envelope, an unknown tool name. Those rejections are not field-and-hint shaped. A client that fails to parse the field=…hint=…: prefix out of an error string should fall back to treating the string as an opaque message, not raise a parse error of its own.

  • MCP Tools reference — the tool argument tables, including the memory-summary length bound this envelope enforces.
  • CLI guide — the exit-code table this page’s Connect mapping is proven to sit inside.