import { type EngineNotice } from "../core/types.js";
/** Hop-chain axis tags: durable a* handle / sessionId / taskId. Order IS the key-preference order. */
export type PeerAxisTag = "h" | "s" | "t";
/** The five-code closed refusal set (`details.error` values). The first four are gate verdicts;
 *  `queue_full` is judged per delivery leg against that leg's EXISTING observable bound (mailbox
 *  peekCount / the live pre-attach buffer cap) — the gate has no queue axis of its own. */
export type PeerRefusalCode = "rate_limited" | "duplicate" | "hop_loop" | "hop_runaway" | "queue_full";
/** Gate verdict subset of {@link PeerRefusalCode} (what `admit` itself can refuse with). */
export type PeerAdmissionRefusal = Exclude<PeerRefusalCode, "queue_full">;
export interface PeerLaneConfig {
    /** Per-sender burst budget (bucket size). */
    bucketCapacity: number;
    /** Per-sender steady-state refill rate (tokens per second). */
    refillPerSecond: number;
    /** Exact-body dedup window in ms (0 = dedup off — a legal single-axis loosening). */
    dedupWindowMs: number;
    /** Max occurrences of the RECIPIENT's own tokens in a prospective chain before `hop_loop`. */
    maxSelfHops: number;
    /** Max prospective chain length before `hop_runaway`. */
    maxChainLength: number;
    /** Per-recipient sender-state table capacity (LRU). */
    maxTrackedSenders: number;
    /** L4 mailbox occupancy bound (`peekCount` at entry) — the ONE leg-level queue bound this config
     *  owns. The live legs are governed by their existing caps with their existing semantics. */
    maxQueuedPeerMessages: number;
    /** Process-global recipient-instance registry capacity (LRU). Eviction cost = that recipient's
     *  senders restart with full buckets — no correctness debt (the gate holds no state that must
     *  survive across messages). */
    maxTrackedRecipients: number;
    /** Peer WAKE budget, HOST-enforced: how many times per hour a host may start a turn on a session
     *  whose `crossSessionInbound` resolves to `wake` because its box grew. The engine spells the
     *  number and the contract; the host that starts turns is the executor. Integer. */
    maxWakesPerHour: number;
    /** Peer delivery budget, ENGINE-enforced: how many box RECORDS one drain pass injects (held
     *  releases, notice lines, peer messages and engine frames count alike — one counter over every
     *  record injection); the rest stay leased for the next boundary, and the pass's own `inbox_notice`
     *  line — which is not a record — says how many. Integer. */
    maxMessagesPerRound: number;
    /** The `ListAgents` freshness window: a live row with no activity word is listed by default only
     *  while `now − updatedAt` is within it (`includeAll` lists the rest). Milliseconds. */
    listingFreshWindowMs: number;
}
/** Upstream defaults, adopted verbatim (anchored; deviation would need evidence sema does not have). */
export declare const PEER_LANE_DEFAULTS: Readonly<PeerLaneConfig>;
/** Test seam (mirrors `__resetMaterializeEnvAnnouncements`); never called by production code.
 *  Re-arms BOTH arms (a WeakMap has no clear — it is re-minted). */
export declare function __resetPeerLaneRangeAnnouncements(): void;
/**
 * Resolve a caller's partial config against the defaults, per-field range-clamped-to-default.
 * There is no off switch: the guard is always on; the ranges ARE the host's tuning space.
 *
 * A field the resolver cannot use falls back to ITS default (never a clamp to the nearest edge, and
 * never a whole-config reject) — and, since #551, says so: one `config.peer_admission_out_of_range`
 * notice PER FIELD carrying `{ field, given, default, range, reason }`. Both silent arms are covered:
 * a value outside the legal window, and a value that is not a finite number at all (a string off an
 * untyped host's JSON wiring, `NaN`, `Infinity`). Two bad fields in one call are two notices — the
 * operator has two things to fix and folding them into one line would hide the second. `onNotice` is
 * the deployment's sink; absent, the announcement still reaches `console.warn` through the shared
 * delivery throat (an unwired host is not a silent one).
 */
export declare function resolvePeerLaneConfig(overrides?: Partial<PeerLaneConfig>, onNotice?: (notice: EngineNotice) => void): PeerLaneConfig;
/** Hop-chain carrier sliding window: a chain keeps its most recent entries (upstream `Xbr`=32).
 *  The chain grows ONLY at send time (the sender appends its own token); delivery/replay never
 *  appends — a message-driven revival is not a hop. */
export declare const PEER_HOP_CHAIN_WINDOW = 32;
/**
 * One hop token — a stable identity string with an axis label, NOT an HMAC fingerprint (deliberate
 * simplification of upstream's process-random-key form, with a gain: sema's chain never reaches a
 * model face and only travels inside engine-minted typed records whose readers already hold the ids,
 * so there is nothing to hide — while a stable form keeps self-loop detection closed ACROSS process
 * restarts, which upstream's process-local key cannot do). JSON-tuple form is injective for any
 * scope/value content (same family as the send-message target-lane key).
 */
export declare function peerAxisToken(scope: string | undefined, axis: PeerAxisTag, value: string): string;
/** Append the sender's own token to an inbound chain, keeping the most recent
 *  {@link PEER_HOP_CHAIN_WINDOW} entries. `undefined` token (an identity-less direct mount that
 *  cannot mint one) forwards the chain unchanged — it cannot honestly claim a hop identity. */
export declare function appendHopToken(chain: readonly string[], token: string | undefined): string[];
/** A run's peer-identity snapshot: the frozen canonical key plus the monotonic union of every axis
 *  token the run has ever worn (old-cycle tokens keep matching after an axis re-derives). */
export interface PeerIdentity {
    /** The identity's scope axis (folded into every token). Set once at ref creation. */
    scope?: string;
    /** Canonical key — the FIRST recorded axis wins and then freezes (axes are recorded in h→s→t
     *  preference order by the engine's own call sites: a durable child's a* handle lands before its
     *  session/task axes, so a durable agent keys `h` on every cycle). Used as the sender bucket key
     *  AND as the token the sender appends when forwarding. */
    key?: string;
    /** Monotonic union of all recorded axis tokens — the recipient-side self-token set. */
    ownTokens: string[];
}
/**
 * design/176 §4.1 — the LATE-BOUND self-identity carrier ({@link import("../core/runner/contracts.js").RunInternals}`.peerSelfRef`).
 * A ref (same family as `ownOrgAdmissionRef`): revival replays a spread COPY of spawn-time
 * internals, so a plain field would freeze at its spawn value; and a root run's session axis only
 * exists once `prepareTask` acquires the session — no single assembly point can synthesize the full
 * value. The ref is created (possibly empty) where internals are assembled, then axes are recorded
 * at their birth points through the ONE monotonic operation {@link PeerSelfRef.addAxis}. Readers
 * (sender key, sender hop token, recipient own-token set) always read `current` at call time.
 */
export interface PeerSelfRef {
    readonly current: PeerIdentity;
    /** Record an axis (monotonic union; first call freezes the canonical key). Idempotent per
     *  (axis,value); empty values are ignored. */
    addAxis(axis: PeerAxisTag, value: string): void;
}
export declare function createPeerSelfRef(scope?: string): PeerSelfRef;
/** A shared mutable INBOUND-chain ref (`Zbr` counterpart): overwritten at the harness consumption
 *  boundary with the chain of the peer payload the model just consumed — enqueue-time writes would
 *  let an outbound call inherit the chain of a message the model has not seen yet. New cycles start
 *  from their seed (L3 chain parameter / L4 `peerSeed`); no seed = empty chain. */
export interface PeerInboundChainRef {
    current: string[];
}
export declare function createPeerInboundChainRef(seed?: readonly string[]): PeerInboundChainRef;
/** design/385 §4.6 — how many admitted (seq → body) pairs a sender's replay ledger keeps: more than
 *  any one leased batch a drain injects between two acks (a batch is at most the queued-message bound,
 *  50 by default), so an ack fault re-presents every message of the batch as a re-presentation. */
export declare const REPLAY_LEDGER_MAX = 128;
export interface PeerAdmissionRequest {
    /** The sender's canonical identity key ({@link PeerIdentity.key}), or undefined for an
     *  identity-less direct mount — rate/dedup are then SKIPPED (there is no honest bucket key),
     *  while hop and the leg-level queue bounds still apply (upstream's unidentifiable-sender arm). */
    senderKey: string | undefined;
    /** The exact message body (dedup is exact-match on this). */
    body: string;
    /** The PROSPECTIVE outbound chain — inbound chain + the sender's own token already appended
     *  (equivalent to upstream's append-at-send-then-check-at-receipt order; a self-send counts its
     *  own appended token, no off-by-one). */
    prospectiveChain: readonly string[];
    /** The RECIPIENT's self-token set, as knowable at this entry point. */
    ownTokens: readonly string[];
    /** design/385 §4.6 — the mailbox seq of a DRAIN-stage re-check (absent at the send point). Dedup
     *  judges body+seq: same body, same seq ⇒ a re-presentation of an already-admitted message (pass);
     *  same body, other seq inside the window ⇒ duplicate. No shared state across consumers is needed:
     *  a different consumer process holds a fresh gate and admits the redelivery by construction. */
    seq?: number;
}
export type PeerAdmissionVerdict = {
    ok: true;
} | {
    ok: false;
    reason: PeerAdmissionRefusal;
};
export interface PeerAdmission {
    /** One admission judgment: hop first, then dedup, then bucket. Success charges the bucket and
     *  records the body IN THIS CALL (no rollback protocol — a downstream delivery failure does not
     *  refund; the bucket refills on its own). Refusal has zero side effects. Config is read
     *  per call (state is long-lived, values are current — upstream form). */
    admit(req: PeerAdmissionRequest, config: PeerLaneConfig): PeerAdmissionVerdict;
    /** Hop-only check (the identity-less-sender arm, and any caller that needs the chain verdict
     *  without touching rate/dedup state). */
    checkHopChain(prospectiveChain: readonly string[], ownTokens: readonly string[], config: PeerLaneConfig): PeerAdmissionVerdict;
    /** Observation face: senders currently tracked by this recipient instance. */
    trackedSenderCount(): number;
    /** Observation face: refusals by reason since this instance was created. */
    refusalCounts(): Readonly<Record<PeerAdmissionRefusal, number>>;
}
export interface PeerAdmissionOptions {
    /** Injectable clock (tests). */
    now?: () => number;
}
export declare function createPeerAdmission(options?: PeerAdmissionOptions): PeerAdmission;
export declare function peerAdmissionFor(scope: string | undefined, recipientKey: string, config: PeerLaneConfig, options?: PeerAdmissionOptions): PeerAdmission;
/**
 * The delivery legs' ONE admission entry: registry seat-commit follows the SAME
 * refusal-is-side-effect-free rule as the sender table inside `admit` — the instance is looked up
 * WITHOUT an LRU touch (a detached fresh one serves a first-contact recipient), judged, and only an
 * ADMITTED message commits the seat (insert + touch + bounded eviction). A refusal to a
 * never-tracked recipient must not evict an active recipient's rate/dedup state (`peerAdmissionFor`
 * always commits a seat, which is right for a host asking for the instance itself, and wrong at a
 * gate that has not admitted anything yet).
 */
/** design/385 §4.6 — the two-point gate's STAGE discriminator. The cross-session lane judges a message
 *  twice, in two processes or (single-process hosts) in one: at the SEND point before the append, and at
 *  the recipient's DRAIN point before injection. The registry is process-global and keyed by
 *  (scope, recipient), so without a stage in the key the drain re-check of a message this same process
 *  sent would read the send-point body record and refuse its own message as `duplicate`. Two gate
 *  instances, one per stage; the stage-less key is the pre-385 spelling and stays byte-identical. */
export type PeerAdmissionStage = "send" | "drain";
export declare function judgePeerAdmission(scope: string | undefined, recipientKey: string, req: PeerAdmissionRequest, config: PeerLaneConfig, options?: PeerAdmissionOptions, stage?: PeerAdmissionStage): PeerAdmissionVerdict;
/** Test seam ONLY (module-level registry hygiene between test files — same posture as the lane map,
 *  which drains itself; this one is long-lived by design so tests reset it explicitly). */
export declare function resetPeerAdmissionRegistryForTests(): void;
/**
 * design/176 §3.4 — the PER-MESSAGE discipline block, minted frame-adjacent at the message ASSEMBLY
 * points (send-message-tool.ts is the single producer of every peer carrier, so assembly-side
 * mounting is structurally single-sourced and survives any downstream projection of the payload).
 * Layered WITH the session-level `SUBAGENT_CONSENT_NOTICE` (prompts/default.ts), not replacing it:
 * that section governs COMMAND authority at session level; this block narrows AUTHORIZATION per
 * message. Placement per leg: OUTSIDE the `<teammate-message>` frame (a caller-declared copy inside the
 * sender-controlled body arrives escaped, so position distinguishes the real block); the L3 resume
 * leg carries it in the TRUST-FRAME layer of the peer-resume prompt, never inside the data fence
 * (a load-bearing instruction inside a "this is DATA" fence would demote itself).
 */
export declare const PEER_MESSAGE_NOTICE: string;
