import type { CorruptReadInfo } from "../stores/file/corrupt-read-sink.js";
/** One persisted name→agent binding (the CC roster `members[{name, agentId}]` shape + sema access axes). */
export interface RosterEntry {
    /** The verbatim spawn name (display form; resolution runs over {@link normalizeAgentName}). */
    name: string;
    /** The background agent's task id (`a…`). */
    agentId: string;
    /** The retained child session id, when known — the cross-run resume address. */
    sessionId?: string;
    /** The spawn's parent tool-call id (retain-ledger key), when known. */
    toolUseId?: string;
    /** the ROOT host session of the delegation tree at spawn (recovery-face grouping
     *  key; equals the spawner's session at depth 1). Stored verbatim; no predicate arm consumes it
     *  yet (enumeration/recovery is the reader). */
    rootSessionId?: string;
    /** design/151 S3b — the model RECORD KEY at spawn (revival lookup key; display/model-routing hints
     *  only, never a serialized spec). NOT the resolved model id: a spawn that named a model in WORDS
     *  records the caller's spelling VERBATIM (a catalog key, tier word or CC alias), because a revival
     *  re-resolves this value against the catalog in force at WAKE time and a resolved id would only
     *  ever resolve again on deployments whose catalog keys happen to equal model ids. A spawn that
     *  carried a Model OBJECT (definition/tool-level/inherited ref) records that object's id, which on
     *  such a deployment is not a catalog key and degrades softly on the revival lane. */
    model?: string;
    /** Ruled 2026-08-05: the spawn requested a model word that did not bind — the row runs on its
     *  inherited default. Closed set, single member today; absent = bound normally (or no word). */
    modelFallback?: "inherit_no_tier_binding";
    /** Spawn owner (task/session id) — consumers re-apply access checks against these two axes.
     *  A-2: REQUIRED (default-deny predicate; an axis-less row would be unreachable). */
    owner: string;
    /** Spawn principal scope. A-2: REQUIRED (`"default"` is the single-tenant spelling). */
    scope: string;
    /** mirrors the live registry's explicit session-scoping flag: session-ID owner matching
     *  is permitted ONLY when the spawn was session-scoped (TaskRegistry.canAccess parity — without
     *  the flag, a task-scoped agent that fell out of the live registry would become visible through
     *  the durable fallback to same-session callers the registry itself rejects). */
    sessionScoped?: boolean;
    /** Registration wall-clock ms (latest-wins tiebreak, matching registry layer 0). */
    createdAt: number;
}
/**
 * The caller's access axes for a roster read — the SAME predicate the live task registry applies
 * (`canAccess`): scope mismatch = invisible; then owner match, or session match. (S1
 * review): resolution MUST filter the candidate pool by access BEFORE the latest-wins reduce —
 * reduce-then-check lets one tenant's newer same-name entry shadow (and thereby suppress) another
 * tenant's older authorized binding. Implementations (incl. pg/tidb) apply this in the query.
 */
export interface RosterAccess {
    owner?: string;
    scope?: string;
    sessionId?: string;
}
/** Ruled 2026-07-22: DEFAULT-DENY both axes (TaskRegistry.canAccess byte-parity —
 *  the polarity split was the defect). Entries are written with both axes (spawn chain guarantees
 *  it; {@link RosterEntry} requires them), so a missing axis is a broken row, answered with a miss. */
export declare function entryAccessible(e: RosterEntry, access: RosterAccess): boolean;
/**
 * The roster persistence seam. All methods MAY be async (a pg-backed store is); core's two bundled
 * implementations are synchronous. Implementations are ADVISORY infrastructure: a throwing store
 * must never fault a spawn — callers wrap writes in try/catch and treat read errors as a miss.
 *
 * v1 honesty contract (RB-23): writes are FIRE-AND-FORGET from the spawn path — a named-spawn
 * receipt does NOT acknowledge roster persistence, and a failed/lost async write degrades to a
 * later `not_found`/`roster_only` miss (never a wrong answer). Acknowledged writes (outbox) are the
 * DEPLOYMENT'S half: they need a durable spool + retry scheduling, which belongs to whoever owns
 * the database connection lifecycle — core's advisory contract above is the library-side final form.
 *
 * RB-23② release semantics (settled): a roster row's lifetime follows ADDRESSABILITY — the durable
 * session data behind `sessionId` — NOT the agent's running state and NOT the parent-run retain
 * ledger. Core deliberately wires NO terminal-cleanup or retain-evict call into {@link
 * RosterStore.releaseAgent}: retain is a parent-run-scoped cache (disposed with the run), while the
 * roster exists precisely to outlive it; releasing at terminal would kill cross-run resolution, the
 * store's whole purpose. The correct trigger is durable session DELETION, which happens on the
 * deployment side — a deployment's session GC calls `releaseAgent` (or joins roster rows against
 * session liveness in SQL). A stale row left behind costs only an honest downstream failure on
 * resume (miss / `resume.evicted`-class error — never a wrong answer); {@link RosterGcOptions}
 * `maxAgeMs` is the bounded-staleness backstop for deployments that want one (RB-23③), and
 * {@link RosterGcOptions.maxEntries} the bounded-SIZE one (HRD-AGO-11). Both are OFF by default: a bundled
 * store keeps every live (name, owner, scope) binding forever, so a deployment that spawns under a fresh owner
 * per run grows one permanent row per spawn until it opts into a bound. That default is deliberate — an
 * evicted row is a durable ADDRESS a caller can no longer reach — but it is the deployment's to change.
 */
export interface RosterStore {
    /** Record (or supersede — latest-wins per normalized name is applied at READ time) a binding. */
    record(entry: RosterEntry): void | Promise<void>;
    /** Resolve a name for a CALLER: filter by {@link RosterAccess} FIRST, then latest `createdAt`
     *  wins within the authorized pool (filter-before-reduce). Undefined = miss. */
    resolve(name: string, access: RosterAccess): RosterEntry | undefined | Promise<RosterEntry | undefined>;
    /** All live entries (diagnostics / deployment listing; order unspecified). */
    list(): RosterEntry[] | Promise<RosterEntry[]>;
    /** Drop every binding for an agent id (agent stopped and unaddressable / deployment GC). */
    releaseAgent(agentId: string): void | Promise<void>;
}
/**
 * RB-23③ (settled): optional bounded-staleness GC for the bundled stores. `maxAgeMs` is a
 * DEPLOYMENT knob because the right value is the deployment's durable-session retention horizon
 * (align the two and a roster row can never outlive the address it points at) — core cannot know
 * it, so the default is NO TTL (current behavior; the file store's upsert already bounds growth by
 * live name cardinality, not spawn count). Semantics: an expired row is a MISS on every read
 * (resolve/list) and is opportunistically dropped on the next write — same honesty class as every
 * other degradation here (expired ⇒ miss, never a wrong answer). A pg-backed store implements the
 * same two arms in SQL (`WHERE created_at > now() - ttl` + periodic delete).
 */
export interface RosterGcOptions {
    /** RB-473 — disclosure channel for CONTAINED host-callback failures in this scope (the RB-463
     *  isolation primitive's bounded first-failure per site). Absent ⇒ failures are counted only. */
    onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
    maxAgeMs?: number;
    /**
     * HRD-AGO-11 (opt-in) — hard cap on live rows in a bundled store. **Default: UNBOUNDED**, and deliberately
     * so: the upsert domain is (normalized name, owner, scope), which bounds growth by LIVE NAME CARDINALITY
     * only — a deployment that spawns `worker` under a FRESH owner per run (a per-task owner id is the common
     * shape) accumulates one permanent row per spawn, and the file store rewrites the whole document on every
     * write, so the cost is O(n²) over a deployment's lifetime. A blind default cap would be worse than the
     * leak: a roster row IS a durable ADDRESS, and silently evicting one turns a live agent unaddressable
     * (`not_found` on a name the caller legitimately holds). So the bound is the DEPLOYMENT's call — it is the
     * only party that knows how many live addresses it needs — and this is how it says so.
     *
     * Policy when set: after each write, while the row count exceeds `maxEntries`, drop the row with the
     * SMALLEST `createdAt` (oldest binding first; the row just written is by construction the newest, so a
     * write never evicts itself). Every dropped row is reported to {@link onEvicted} — an evicted address is a
     * capability the deployment loses, so it is never dropped in silence. Reads are unaffected (an evicted row
     * is simply gone: an honest miss, never a wrong answer — the same honesty class as `maxAgeMs`).
     *
     * A pg/tidb-backed store implements the same shape in SQL (a windowed delete keyed on `created_at`).
     */
    maxEntries?: number;
    /** HRD-AGO-11 — called with each row dropped by the {@link maxEntries} cap (never for a normal upsert
     *  supersede). A throwing sink is swallowed: roster writes are advisory and must never fault a spawn. */
    onEvicted?: (entry: RosterEntry) => void;
}
/** In-memory roster (the default): cross-turn within one process, gone on restart. */
export declare class MemoryRosterStore implements RosterStore {
    private entries;
    private readonly maxAgeMs;
    private readonly gc;
    constructor(opts?: RosterGcOptions);
    record(entry: RosterEntry): void;
    resolve(name: string, access: RosterAccess): RosterEntry | undefined;
    list(): RosterEntry[];
    releaseAgent(agentId: string): void;
}
export declare class FileRosterStore implements RosterStore {
    private readonly path;
    private readonly maxAgeMs;
    private readonly gc;
    private readonly onCorruptRead;
    constructor(path: string, opts?: RosterGcOptions & {
        onCorruptRead?: (info: CorruptReadInfo) => void;
    });
    /** RB-482 #25 (baggage class-4 ruling): the LENIENT read grade's swallow stays (a store fault must read as a
     *  miss on resolve/list — advisory layer), but it is no longer SILENT: an IO failure or corrupt
     *  document was indistinguishable from an honestly empty roster. Never fires on plain ENOENT. */
    private discloseCorrupt;
    /** two read grades: MISSING file (ENOENT) is an honest empty roster, but a corrupt or
     *  transiently unreadable file must THROW on the mutation path — treating it as empty would let
     *  the next `record` atomically REPLACE the existing file with just one row (silent data loss).
     *  Read paths (resolve/list) degrade the throw to a miss via `lenient`. */
    private read;
    private write;
    record(entry: RosterEntry): void;
    resolve(name: string, access: RosterAccess): RosterEntry | undefined;
    list(): RosterEntry[];
    releaseAgent(agentId: string): void;
}
/** design/147 S1c — advisory durable-roster write for a NAMED background spawn: never throws and never
 *  unhandled-rejects (sync or async store alike); a roster fault must not fault the spawn.
 *  design/157 A7 — moved from `agents/subagent.ts`: the semantics are roster's, not the delegation
 *  tool's (the signature already value-imports `RosterStore`/`RosterEntry` from this module). */
export declare function recordRosterSpawn(roster: RosterStore | undefined, entry: RosterEntry, onError?: (err: unknown) => void): void;
