/**
 * What a failed API call TELLS THE AGENT TO DO.
 *
 * WHY THIS MODULE EXISTS. Until now every non-2xx became one sentence:
 *
 *     Mnemoverse API error 401: {"code":"UNAUTHORIZED","message":"Invalid or
 *     revoked API key.","requestId":null,"retryable":false,"details":null}
 *
 * The `isError` flag and the status code were right; the TEXT was a raw echo of
 * a wire body. Verified against production with a real `tools/call`
 * (2026-08-16): with a bad key, `memory_write` returns exactly that. The reader
 * of a tool result is a MODEL, and that string gives a model nothing to act on,
 * so the two things it actually does are relay "error 401" to the user or guess
 * — and the cheapest guess is "the network is down", which sends the user to
 * debug a working network while a placeholder key sits in their config.
 *
 * Every message here therefore answers three questions in order: what happened,
 * WHOSE problem it is, and what to do next — including, explicitly, whether to
 * retry. "Do not retry" is not decoration: an agent that loops on a 429 turns
 * one rate-limit into a sustained one, and an agent that loops on a 401 burns a
 * conversation on a key that will never start working.
 *
 * THE RAW BODY IS NEVER DROPPED. It is appended after the guidance, still
 * carrying the literal `Mnemoverse API error <status>` that this repo's tests
 * and anyone's grep already look for. Making the text kind to an agent must not
 * make it useless to a human debugging at 3am.
 *
 * WHAT THE STATUS CODES ACTUALLY MEAN HERE, verified by reading the engine
 * (mnemoverse-core `src/mnemo/api/`, 2026-08-16) rather than assumed from HTTP
 * folklore. The distinctions below are the entire point of this file:
 *
 *   401  auth.py — "Invalid or revoked API key." / "Missing API key." The key.
 *        (Read on 2026-08-16. Since 2026-09-20 the engine answers a sentence
 *        per cause and adds `details.reason` and `details.keys_url`; the old
 *        sentence is what an engine without that change still sends, which is
 *        why explain401 keeps the substring branch as its fallback.)
 *   403  routes.py, rooms_routes.py, auth.py — "Room is archived", "Not an
 *        active member of this room", "Read-only membership cannot write to
 *        this room", "Invalid room address", "You do not own this room", plus
 *        the OIDC scope rules. NOT a bad key: the caller was identified and
 *        then refused. Telling the user to replace a working key over a 403
 *        would be a new, more confident lie than the raw echo was.
 *   404  A room that does not exist for this key — or, when the body says
 *        nothing at all, a path the deployment does not serve. A DOMAIN never
 *        404s: an empty domain reads as empty, so "your domain is missing" is
 *        the wrong guess and is named as wrong below.
 *   429  THREE different causes with opposite advice. rate_limit.py returns
 *        `retryable: true` with a `Retry-After` header (waiting works);
 *        usage.py (daily quota) and subscription_guard.py (atom limit, payment
 *        past due) return `retryable: false` (waiting does NOT work — the
 *        account needs an upgrade or a new day). A blanket "wait and retry"
 *        would be wrong for two of the three.
 *   5xx  auth.py returns 503 `retryable: true` when its DB pool is gone. Ours,
 *        not theirs.
 *
 * A 2xx CAN FAIL TOO, and gets the same treatment for the same reason. A reply
 * whose body this client cannot parse is not a status at all, so none of the
 * sentences above fit it — and it used to borrow the TRANSPORT sentence, which
 * asserts that nothing answered. `explainUnreadableBody` at the bottom of this
 * file owns that case now; the distinction it protects is "a reply arrived and
 * was unreadable" versus "no reply arrived", which have opposite fixes.
 *
 * RELATION TO THE STARTUP PROBE (src/index.ts, `probeApiKeyInBackground`, added
 * in 0.8.4). That probe treats 401 and 403 alike, and is right to: it calls
 * `GET /memory/stats`, which addresses no room, so a 403 there can only come
 * from the auth layer. On a TOOL CALL the same status usually comes from a room
 * the caller named, which is why the two surfaces diverge on 403 and must not be
 * "unified" by a later reader. They agree where it matters: same `Mnemoverse:`
 * opener, same `MNEMOVERSE_API_KEY` variable name, same console origin.
 */
/** The console page that issues keys — the one place a user fixes a 401.
 *  Exported so src/requests.ts can point at the same URL from
 *  `refusePlaceholderKey` instead of repeating the literal (the engine is
 *  becoming a second producer of this same URL, `details.keys_url` on a 401
 *  body, which is validated against an allow-list rather than trusted
 *  outright, see `validatedKeysUrl` below, but this constant stays the
 *  fallback for both). */
export declare const KEYS_URL = "https://console.mnemoverse.com/dashboard/keys";
/**
 * How a server that registers these tools wants its failures worded (STEP4-2,
 * STEP4-3, owner decisions 2026-09-24). Every field is optional, and every
 * field's absence means exactly what this file already does today: the
 * whole point is that a consumer supplying no `wording` at all gets
 * byte-identical text to before this type existed.
 *
 *  - `serverNoun`: what the three tool descriptions that name themselves
 *    call this deployment ("this server" vs "this connector"). Read by
 *    src/tools.ts at registration time; ignored by this module.
 *  - `auth`: which credential the CALLER holds, not which one core issued.
 *    "api-key" (default) keeps every existing MNEMOVERSE_API_KEY-flavoured
 *    sentence. "oauth" is for a server whose user never sees an API key at
 *    all (a hosted connector minting a key on their behalf): no 401/403
 *    explanation under this mode names MNEMOVERSE_API_KEY, an env var, an
 *    MCP config file, or the keys console; each says what an OAuth user can
 *    actually do instead (reconnect, sign in again, check the plan, wait
 *    for the retry window).
 *  - `keysUrl`: replaces {@link KEYS_URL} wherever the api-key vocabulary
 *    prints a console URL, for a deployment whose key-management page is
 *    not console.mnemoverse.com. Has no effect under `auth: "oauth"`, which
 *    prints no console URL at all.
 *  - `rawDetail`: whether the wire body's raw tail (STEP4-3; e.g. `Raw
 *    detail — Mnemoverse API error 401 on …`) is appended after the
 *    guidance. Defaults to `true`, unchanged from every release before this
 *    one. Held in reserve for a deployment whose core-side error envelopes
 *    are found to echo request content back in `details`: set to `false`
 *    only once that check finds something to hide.
 */
export interface Wording {
    serverNoun?: "this server" | "this connector";
    auth?: "api-key" | "oauth";
    keysUrl?: string;
    rawDetail?: boolean;
}
/** Everything known about one failed call, at the moment it failed. */
export interface ApiFailure {
    /** HTTP status. */
    status: number;
    /** Response body, verbatim and unparsed. */
    body: string;
    /** Uppercase HTTP verb. */
    method: string;
    /** Path below the API base, e.g. "/memory/write". Deliberately NOT the full
     *  URL: the base can carry credentials, and the path alone is what identifies
     *  the operation. */
    path: string;
    /** `Retry-After` header when the response carried one. */
    retryAfter?: string | null;
}
/**
 * The engine's error envelope, as much of it as survived parsing.
 *
 * Every field is optional because a non-2xx does not have to come from the
 * engine at all: a proxy, a tunnel, or a wrong `MNEMOVERSE_API_URL` answers with
 * HTML, plain text or nothing. A missing field means "the body did not say",
 * never a default — inventing `retryable: false` for an unparseable body would
 * put a retry decision on evidence that does not exist.
 */
export interface ErrorEnvelope {
    code?: string;
    message?: string;
    retryable?: boolean;
    /**
     * `details.reason` on a 401 (an engine change not yet released as of
     * this one): missing_key | placeholder_key | revoked_key | invalid_key |
     * malformed_key. Absent on every 401 a currently-released engine sends, and
     * absent whenever `details` was not an object carrying a string `reason`.
     * Never defaulted, for the same reason nothing else in this interface is:
     * a made-up reason is a wrong instruction with a confident source.
     */
    reason?: string;
    /**
     * `details.keys_url` on the same 401, already validated by
     * {@link validatedKeysUrl}: present only when it was an https URL whose
     * host is exactly console.mnemoverse.com. A response body must not be able
     * to point a reader at an arbitrary site, so an untrusted value never
     * reaches this field at all; a caller that wants the URL for a reason
     * branch falls back to {@link KEYS_URL} when this is undefined.
     */
    keysUrl?: string;
}
/**
 * Read the engine's envelope out of a response body.
 *
 * Two shapes are accepted because core produces both: its own middleware writes
 * `{code, message, retryable, details}` at the top level, while anything raised
 * as a FastAPI `HTTPException` arrives wrapped as `{"detail": …}` — where the
 * detail is sometimes a string and sometimes the envelope again. The feed's
 * 404-vs-404 test in this repo pins the nested form, so both are real.
 *
 * `details` (plural, a sibling of `code`/`message`/`retryable`) is read here
 * too, for `reason` and `keys_url`. Anything else inside
 * it is ignored silently: this parser reads a fixed, named set of fields and
 * has no way to tell a future field the engine adds from noise, so silence is
 * the only honest answer for either one.
 */
export declare function parseErrorEnvelope(body: string): ErrorEnvelope;
/** `Retry-After` as whole seconds, when it is a plain number. The HTTP-date
 *  form is legal too, and is deliberately NOT parsed: an unparsed header
 *  degrades into "wait a moment", which is honest, whereas a misparsed date
 *  would print a confident and wrong number of seconds. */
export declare function retryAfterSeconds(header: string | null | undefined): number | undefined;
/**
 * The agent-facing explanation for one failed call, with the raw body kept
 * after it (unless `wording.rawDetail === false`, STEP4-3).
 *
 * Total by construction: every status reaches a sentence, and the fallback says
 * that it has no specific guidance instead of inventing some.
 *
 * `wording` (STEP4-2/3, owner 2026-09-24) is optional and, absent, resolves
 * to exactly today's behaviour: every call site before this release passes
 * none, so every existing pin stays byte-identical.
 */
export declare function explainApiFailure(f: ApiFailure, wording?: Wording): string;
/**
 * The request never got an answer at all: DNS, connectivity, a host that does
 * not listen, or this client's own probe deadline firing.
 *
 * Included even though the brief was about HTTP statuses, because it is the
 * OTHER half of the same defect and the two are easy to confuse. The 401
 * message above tells an agent not to blame the network; this is the case where
 * the network genuinely IS the answer, and `TypeError: fetch failed` — which is
 * all a model saw before — is no more actionable than the raw 401 body was.
 *
 * A timeout is named separately because the advice differs in one word: an
 * unreachable host is unlikely to become reachable in three seconds, whereas a
 * request that ran out of time may well succeed on a second try.
 *
 * NARROWED to `fetch()` itself rejecting. Reading the BODY of a response that
 * did arrive used to land here too, so a 200 carrying a sign-in page printed
 * "no HTTP response came back" — see {@link explainUnreadableBody}, which owns
 * that case now. Every sentence below asserts that nothing answered, so it may
 * only be reached when nothing did.
 */
export declare function explainNetworkFailure(method: string, path: string, cause: unknown, wording?: Wording): string;
/** A 2xx-or-not response whose BODY this client could not turn into the JSON
 *  this API speaks. The status is known — a reply arrived — which is the whole
 *  difference between this and {@link ApiFailure} or a transport failure. */
export interface UnreadableBody {
    /** HTTP status, as it actually arrived. */
    status: number;
    /** Uppercase HTTP verb. */
    method: string;
    /** Path below the API base. Never the full URL — the base can carry
     *  credentials, and the path alone identifies the operation. */
    path: string;
    /** The bytes that failed to parse, when they were read at all. Absent when
     *  the body READ itself failed, which is a different sentence below. */
    bodyPreview?: string;
    /** The SyntaxError, TypeError or abort that stopped the read. */
    cause: unknown;
}
/**
 * A reply ARRIVED and could not be read — which is not a dead network.
 *
 * WHY THIS IS SEPARATE FROM {@link explainNetworkFailure}. Both failures used
 * to throw the same NetworkError, so `JSON.parse` choking on a 200 printed
 * "the memory service could not be reached at all — POST /memory/read failed
 * before any HTTP response came back… That is a connectivity or DNS problem".
 * Against a captive portal, a MITM proxy, an SPA that serves its shell with a
 * 200, or a body that stops mid-stream, every clause of that is false: a reply
 * came, with a status, and the Raw detail underneath it quoted a SyntaxError
 * out of the body it had just called nonexistent.
 *
 * The cost is the same one this module exists to remove — a confident wrong
 * cause. "Connectivity or DNS" sends the user to debug working wifi while a
 * portal or a mis-set MNEMOVERSE_API_URL answers every request with a page.
 *
 * WHAT THIS MESSAGE MAY CLAIM. Only what the status proves: something answered,
 * and its answer is not this API's JSON. It does NOT name who answered — this
 * client cannot tell the engine from a gateway in front of it — and it does NOT
 * say whether the operation ran, because a body it cannot read is no evidence
 * either way. That last clause matters most for a write.
 */
export declare function explainUnreadableBody(f: UnreadableBody, wording?: Wording): string;
/**
 * {@link explainUnreadableBody} as an error, so a caller can tell "a reply
 * arrived and was unreadable" from "no reply arrived" by TYPE rather than by
 * matching a sentence — the same reason {@link ApiError} carries fields.
 */
export declare class UnreadableBodyError extends Error {
    readonly status: number;
    readonly method: string;
    readonly path: string;
    /** The bytes that failed to parse, when they were read at all. Kept so
     *  {@link withWording} re-renders the same arm of the explanation. */
    readonly bodyPreview: string | undefined;
    /** `wording` (STEP4-2/3): optional, and absent (every call site before this
     *  release) reproduces today's message exactly: see {@link explainUnreadableBody}. */
    constructor(f: UnreadableBody, wording?: Wording);
    /** The same failure, explained under `wording`: see {@link ApiError.withWording}. */
    withWording(wording: Wording | undefined): UnreadableBodyError;
}
/** {@link explainNetworkFailure} as an error, so `instanceof` can tell a
 *  transport failure from an HTTP one without reading either message. */
export declare class NetworkError extends Error {
    readonly method: string;
    readonly path: string;
    /** `wording` (STEP4-2/3): optional, and absent (every call site before this
     *  release) reproduces today's message exactly: see {@link explainNetworkFailure}. */
    constructor(method: string, path: string, cause: unknown, wording?: Wording);
    /** The same failure, explained under `wording`: see {@link ApiError.withWording}. */
    withWording(wording: Wording | undefined): NetworkError;
}
/**
 * A failed API call, as an ERROR OBJECT rather than a parsed string.
 *
 * The status and the body are fields because a caller that needs to branch on
 * them must not do it by matching the message. `memory_list_recent` used to
 * decide between "the endpoint is not deployed" and "this room does not exist"
 * with `message.startsWith("Mnemoverse API error 404:")`, which coupled a
 * behavioural branch to the exact prefix of a user-facing sentence — so
 * improving the sentence, which is this change, would have silently flipped
 * that branch. Structured fields make the wording free to change.
 */
export declare class ApiError extends Error {
    readonly status: number;
    readonly body: string;
    readonly method: string;
    readonly path: string;
    /** The engine's envelope, already parsed — so a caller never re-parses. */
    readonly envelope: ErrorEnvelope;
    /** The `Retry-After` header as it arrived, when the response carried one.
     *  Kept so {@link withWording} re-renders a 429 with its retry sentence. */
    readonly retryAfter: string | null | undefined;
    /** `wording` (STEP4-2/3): optional, and absent (every call site before this
     *  release) reproduces today's message exactly: see {@link explainApiFailure}. */
    constructor(f: ApiFailure, wording?: Wording);
    /**
     * The same failure, explained under `wording`: a new instance of this
     * class whose message is what the constructor would have produced with
     * that `wording`, every field and {@link isBare404} unchanged. The message
     * is a pure function of the failure and the wording, so re-rendering an
     * instance under the wording it was built with gives the same text back.
     * This is how `MemoryToolDeps.wording` reaches an error the consumer's
     * `apiFetch` built without it: see {@link rewordFailure}.
     */
    withWording(wording: Wording | undefined): ApiError;
    /**
     * A 404 the ENGINE did not answer: silence, or the router's literal
     * defaults. This is what a path the deployment does not serve looks like
     * — the first version of this predicate tested silence alone, mistook the
     * real framework body `{"detail":"Not Found"}` for an engine answer, and
     * un-fired the feed's degrade branch for the exact rollout case it exists
     * for (panel, #93).
     *
     * NAMED FOR WHAT IT TESTS, not for what it implies: a gateway, a proxy or a
     * wrong MNEMOVERSE_API_URL can produce the same shapes and is
     * indistinguishable from here. Known, accepted edge: a foreign API whose
     * 404 nests its error under a key this parser does not read
     * (`{"error":{…}}`) also reads as bare, so the feed degrades to its
     * not-supported notice instead of surfacing the foreign body — bounded
     * harm, since against a wrong base URL the very next call fails with the
     * instructive no-envelope wording.
     */
    get isBare404(): boolean;
}
/**
 * Re-explain one of this module's three errors under `wording`; hand
 * anything else back untouched.
 *
 * This is how `MemoryToolDeps.wording` reaches the error text (STEP4-2): a
 * consumer's `apiFetch` constructs `ApiError`, `NetworkError` and
 * `UnreadableBodyError` however it likes, and `registerMemoryTools` passes
 * every rejection through here with the `wording` it was given, so the
 * consumer states its wording once, on `deps`, and the constructors need no
 * second copy. Re-rendering is idempotent (see {@link ApiError.withWording}),
 * so an `apiFetch` that does pass the same `wording` to the constructors
 * gets the same text either way; one that passed a different wording gets
 * `deps.wording`, the one source of truth for this registration. A
 * rejection that is none of the three classes (a plain `Error`, an
 * `McpError`) is returned as is: it carries no wording to apply, and its
 * identity may matter to whoever threw it.
 */
export declare function rewordFailure(e: unknown, wording: Wording | undefined): unknown;
