/**
 * toolSessions — the end-signal a tool can be handed, and the tier that fires it.
 *
 * Pattern: Registrar + Template Method for the firing matrix.
 * Role:    tools/ layer primitive. `ToolExecutionContext.onTeardown` registers here;
 *          `Agent.run`'s terminals, `RunnerBase.closeToolSessions` and
 *          `RunnerBase.shutdown` fire it.
 * Emits:   nothing itself. `register()` ANSWERS what it did (started/reused) so
 *          the caller — still inside a stage — can emit with a real
 *          `runtimeStageId`, and teardown REPORTS (see {@link ToolSessionReport})
 *          so the runner can emit the two that fire after the last stage.
 *
 * ── The gap this closes ─────────────────────────────────────────────────────
 * A session-based tool service (a managed code interpreter, a headless browser)
 * is Start → Invoke ×N → Stop. Before 9.7.0 a `Tool` had nowhere to hold the
 * middle of that: `ToolExecutionContext` carried no run or session identity, and
 * nothing in the framework ever said "this is over". So a tool either paid
 * start-up on every call, or held the session in a module-level map — which, in
 * a standing agent serving many people from one process, hands ONE live sandbox
 * to whoever calls next. That is not a leak of memory; it is a leak of a
 * filesystem, an environment and half-run state, across users.
 *
 * ── Why the registration lives on the CONTEXT ───────────────────────────────
 * Not `Tool.dispose()`: a `Tool` is a singleton, built once and shared by every
 * run and every session, so "dispose the tool" cannot mean "dispose this
 * caller's session".
 * Not a lifecycle PORT the consumer wires: that makes the common case (one tool
 * that happens to hold a session) a wiring exercise, and the tool that knows the
 * key is the one that cannot reach the port.
 * `ctx.onTeardown(cleanup, { scope, key })` is the only seam where the key and
 * the resource are both in hand at the same instant.
 *
 * ── The seven laws ──────────────────────────────────────────────────────────
 *  1. **At most once, ever** per registration — the `stopped`-flag mechanism
 *     `strategies/lifecycle.ts` uses for strategies, applied one layer down.
 *  2. **Idempotent by `(tool, scope, key)`, first wins.** A second registration
 *     under the same key does not replace the first — the first is the one
 *     holding the live handle; replacing it would drop that handle on the
 *     floor. The repeat is a TOUCH: it refreshes liveness (that is how the
 *     idle sweep and the LRU learn a session is still in use) and answers
 *     `'reused'`.
 *  3. **Reverse order of registration**, settled with `Promise.allSettled`.
 *  4. **Bounded** by `timeoutMs` (default 5s). Teardown is on the SIGTERM path;
 *     an unbounded `stop()` turns a container stop into a wait for SIGKILL.
 *  5. **Never throws into the run** — but **never silent** either. Every
 *     failure, and every timeout, is reported. "Swallowed AND silent" is what
 *     separates a passive recorder from a resource that did not get released.
 *  6. **Tolerates "already gone."** The far side reaps idle sessions on its own
 *     schedule; a `Stop` on a session AWS already collected is a no-op, not an
 *     incident. It still reports, so the difference is visible.
 *  7. **Nothing live is ever persisted.** A checkpoint carries what survives
 *     `structuredClone`; a session handle does not. A resumed run re-opens.
 *
 * Zero-cost when unused: no tool registers → the runner never builds a tier →
 * the terminal path is one `undefined` check.
 */
import type { RunContext } from '../bridge/eventMeta.js';
import type { ToolExecutionContext } from './tools.js';
/**
 * How long a registered cleanup is allowed to live.
 *
 *  - `'call'`     — until `tool.execute` settles (resolve OR throw). Available
 *                   at every door, including `mcpServe`, where a served call is
 *                   the only unit there is.
 *  - `'run'`      — until the run reaches a terminal that is NOT a pause.
 *                   **A pause is not a terminal**: a check-in on a code
 *                   interpreter is a person deciding, and tearing the sandbox
 *                   down there destroys the exact state the resume needs.
 *  - `'session'`  — until the composition root says the hosting session ended
 *                   (`agent.closeToolSessions({ sessionId })`). Nobody but the
 *                   composition root can know that: a request/reply deployment
 *                   has no end-of-session signal, and inventing one would be a
 *                   library guessing about somebody else's protocol.
 *  - `'shutdown'` — until `agent.shutdown()` (which `standingAgent`'s close
 *                   calls). The backstop under all three.
 */
export type TeardownScope = 'call' | 'run' | 'session' | 'shutdown';
/** Why a cleanup ran. Reported on `agentfootprint.tools.session_closed`. */
export type TeardownReason = 'call-end' | 'run-end' | 'session-end' | 'shutdown' | 'idle' | 'evicted';
/** What a tool says about the cleanup it is registering. */
export interface TeardownOptions {
    /** Default `'run'`. Refused by name when the door cannot honour it — see
     *  {@link ToolExecutionContext.teardownScopes}. */
    readonly scope?: TeardownScope;
    /**
     * Dedup key within `(tool, scope)`. Omitted → the tool gets one registration
     * per scope, which is right for a tool that holds exactly one thing.
     *
     * Derive it with {@link toolSessionKey} rather than by hand: a key that is
     * narrower than the identity it isolates is the cross-binding bug, and a key
     * that is wider is a silent latency change.
     */
    readonly key?: string;
    /** The adapter holding the resource — `CodeRunner.id`, say. Reported so a
     *  row names its backend instead of only its tool. */
    readonly runnerId?: string;
    /** One free-form fact about what was opened (the language, the browser
     *  profile). Reported as-is; never a place for user data. */
    readonly label?: string;
}
/** The call a registration came from — what the firing matrix filters on. */
export interface ToolSessionOrigin {
    /** Actual registration-time event context, retained only in memory. */
    readonly runContext?: RunContext;
    readonly tool: string;
    readonly toolCallId: string;
    /** Absent when the door has no run (`mcpServe`). */
    readonly runId?: string;
    /** Absent unless the run is bound to a hosting conversation. */
    readonly sessionId?: string;
}
/**
 * What `register()` did — a new session, or a call joining one already held.
 *
 * Returned rather than reported, because the two halves of a session's life
 * happen in different places and only one of them has a stage to be stamped
 * with. A start and a reuse happen INSIDE `tool.execute`, where the caller
 * still holds the scope and can emit with the real `runtimeStageId`; a close
 * happens after the run's last stage committed, where nothing does. Reporting
 * both through one channel would have meant stamping a live, mid-stage event
 * with the teardown pseudo-stage — a small lie, and exactly the kind that makes
 * a trace disagree with itself.
 */
export type RegisterOutcome = 'started' | 'reused';
/**
 * A teardown that happened. The runner turns it into
 * `agentfootprint.tools.session_closed` / `_close_failed`.
 */
export interface ToolSessionReport {
    readonly kind: 'closed' | 'close-failed';
    readonly tool: string;
    readonly scope: TeardownScope;
    /** {@link hashSessionKey} of the isolation key — never the key. */
    readonly keyHash: string;
    readonly runnerId?: string;
    readonly label?: string;
    /** Which firing site ran it. */
    readonly reason: TeardownReason;
    /** Wall-clock from registration to close. */
    readonly durationMs: number;
    /** `close-failed` only. */
    readonly error?: string;
    readonly errorClass?: string;
}
/**
 * Derive the isolation key a tool should hold a session under.
 *
 * ONE implementation, exported, because the derivation is the security
 * boundary. Returns `undefined` when the facts the scope needs are absent —
 * which is a refusal to guess, not a failure: the caller decides whether to
 * narrow the scope loudly or refuse the call.
 *
 * ```
 * session →  t=<tenant|_>/p=<principal|_>/s=<sessionId>     requires sessionId
 * run     →  t=<tenant|_>/p=<principal|_>/r=<runId>         requires runId
 * call    →  c=<toolCallId>                                 always available
 * ```
 *
 * **`sessionId` alone must never key a live session.** The hosting port says
 * why in its own words: a `sessionId` "is not identity and must never be
 * trusted as identity on its own: anyone who can reach the host can put any
 * string here, including someone else's." A code interpreter keyed on
 * `sessionId` alone hands a live sandbox — files, environment, half-run state —
 * to anyone who guesses one. Tenant and principal are in the key whenever they
 * exist; a deployment that has no principal is thereby STATING it is
 * single-principal rather than quietly assuming it.
 *
 * `'shutdown'` is not a key scope: it is when everything goes, not a thing to
 * hold one session under. Ask for it and you get `undefined`.
 *
 * @example
 *   const key = toolSessionKey(ctx, 'run');
 *   if (!key) throw new Error("run_code: scope 'run' needs a run …");
 */
export declare function toolSessionKey(ctx: Pick<ToolExecutionContext, 'toolCallId' | 'runId' | 'sessionId' | 'identity'>, scope: TeardownScope): string | undefined;
/**
 * A short, stable digest of an isolation key.
 *
 * The key carries tenant, principal and the hosting `sessionId`. Publishing it
 * on the event wire would put a user identifier into every exporter's payload —
 * so the wire carries this instead, which is enough to JOIN two rows and not
 * enough to name whose they are. `meta.sessionId` already carries the session
 * legitimately (9.4.0); the payload does not repeat it.
 *
 * SHA-256, first 12 hex chars, wherever `node:crypto` resolves. In a browser
 * bundle, where it does not, this falls back to the package's non-cryptographic
 * FNV-1a digest — stated here rather than implied, because a fallback nobody
 * documented is how "hashed" comes to mean less than a reader assumed.
 */
export declare function hashSessionKey(key: string): string;
/** Defaults, named so a test and a docstring cannot drift from the code. */
export declare const TOOL_TEARDOWN_TIMEOUT_MS = 5000;
/** A session untouched this long is swept on the tier's next interaction. */
export declare const TOOL_SESSION_IDLE_MS = 900000;
/** How many live registrations one tier holds before it evicts the coldest. */
export declare const TOOL_SESSION_MAX_LIVE = 64;
export interface ToolSessionTierOptions {
    /** Per-cleanup ceiling. Default {@link TOOL_TEARDOWN_TIMEOUT_MS}. */
    readonly timeoutMs?: number;
    /** Idle ceiling for the lazy sweep. Default {@link TOOL_SESSION_IDLE_MS}. */
    readonly idleMs?: number;
    /** Live-registration ceiling before LRU eviction. Default
     *  {@link TOOL_SESSION_MAX_LIVE}. */
    readonly maxLive?: number;
    /** Where TEARDOWN reports go. The runner wires this to the typed event
     *  dispatcher. Starts and reuses are the caller's to announce — see
     *  {@link RegisterOutcome}. */
    readonly report?: (report: ToolSessionReport, origin: ToolSessionOrigin) => void;
    /** Clock seam — tests drive idle and duration without waiting. */
    readonly now?: () => number;
}
/**
 * The teardown tier for one runner.
 *
 * Holds nothing until a tool registers, and the runner builds one only when
 * that happens — so an agent whose tools hold no sessions pays a single
 * `undefined` check at each terminal and nothing else.
 */
export declare class ToolSessionTier {
    private readonly registrations;
    private readonly timeoutMs;
    private readonly idleMs;
    private readonly maxLive;
    private readonly report;
    private readonly now;
    private seq;
    constructor(options?: ToolSessionTierOptions);
    /** Live registrations. Diagnostics and tests. */
    liveCount(): number;
    /**
     * Resolve once every teardown this tier started IN THE BACKGROUND has
     * settled.
     *
     * Two firings have no caller to await them: the idle sweep and the LRU
     * eviction, both of which are triggered by a SYNCHRONOUS `register()` inside
     * somebody else's `tool.execute`. They cannot be awaited there without making
     * a cleanup's latency the tool's latency, so they run detached — and this is
     * how a shutdown path, or a test, joins them.
     */
    settled(): Promise<void>;
    /** Fires nobody is awaiting — see {@link settled}. */
    private readonly background;
    /** Start a detached fire and keep it joinable. */
    private detach;
    /**
     * Register a cleanup, or TOUCH the one already holding this key (law 2).
     *
     * Synchronous and total: it never throws, because it runs inside somebody
     * else's `tool.execute`. Scope support is judged one layer up, where
     * `teardownScopes` is known.
     *
     * @returns `'started'` for a new session, `'reused'` when this call joined
     *   one already held, with `calls` counting how many have now shared it. The
     *   CALLER announces it, because the caller is the one still inside a stage
     *   and able to stamp a real `runtimeStageId` — see {@link RegisterOutcome}.
     */
    register(origin: ToolSessionOrigin, cleanup: () => void | Promise<void>, options?: TeardownOptions): {
        readonly outcome: RegisterOutcome;
        readonly keyHash: string;
        readonly calls: number;
    };
    /** Fire every `'call'` registration this tool call opened. */
    fireCall(toolCallId: string): Promise<void>;
    /**
     * Fire `'run'`-scoped registrations at a run terminal.
     *
     * Called from `Agent.run`/`resume` at a terminal that is NOT a pause — and
     * deliberately not from a `finally`, which also runs on the two pause shapes.
     *
     * **`runId` is optional for unhosted callers.** A `'run'` scope means
     * "release this when the TURN that opened it ends", and a turn is not a
     * runId: a pause and its resume are one turn across two runs (`resume()`
     * builds a fresh executor with a fresh id). Filtering on the id would leave
     * every session a paused turn had opened alive forever — the failure would
     * look like nothing at all, because the run answered fine.
     *
     * Hosted agents use `fireSessionRuns` instead: one runner can retain pauses
     * for several conversations even though only one run executes at a time.
     * A session terminal must never close another conversation's resources.
     *
     * Pass an id where a caller really does mean one specific run.
     */
    fireRun(runId?: string): Promise<void>;
    /** End run resources for one hosting conversation across pause/resume run IDs. */
    fireSessionRuns(sessionId: string): Promise<number>;
    /**
     * Fire the `'session'` registrations for one hosting session — or, with no
     * `sessionId`, every `'session'` registration there is.
     *
     * Answers how many it closed, so a composition root can log a number instead
     * of hoping.
     */
    fireSession(sessionId?: string, reason?: TeardownReason): Promise<number>;
    /** Fire EVERYTHING, whatever scope it asked for. The backstop. */
    fireShutdown(): Promise<number>;
    /**
     * Sweep sessions nobody has touched for `idleMs`.
     *
     * LAZY, on the next tier interaction — never a timer. A library that installs
     * an interval keeps the host process alive, which is the same reason
     * `shutdownOn` refuses to grab signals unless asked.
     */
    sweepIdle(): void;
    private evictOverflow;
    /**
     * The one firing implementation: select, claim (law 1), reverse (law 3),
     * settle everything (law 5).
     *
     * Selection and claiming happen SYNCHRONOUSLY before the first `await`, so
     * two overlapping fires — a shutdown racing an idle sweep — cannot both take
     * the same registration.
     */
    private fire;
    /** One cleanup, bounded, reported either way. Never rethrows. */
    private run;
}
/** A teardown that outran its budget. Named so an alert can route on it. */
export declare class ToolTeardownTimeoutError extends Error {
    readonly timeoutMs: number;
    constructor(what: string, timeoutMs: number);
}
/**
 * This process no longer holds the handle a conversation's session was reached
 * by, so the call was not sent.
 *
 * Not an expiry: nothing was sent and nothing answered. The transport that held
 * the handle forgot it (`evidence`), and the remote session's fate is unknown
 * (`remote`) — it may still run, and it can no longer be ended from here,
 * because its id went with the handle.
 *
 * The message is one fixed sentence; `cause`, when present, is for the app.
 */
export declare class LocalSessionHandleLostError extends Error {
    readonly code: "ERR_LOCAL_SESSION_HANDLE_LOST";
    /** What proves the loss: the transport no longer holds the handle. */
    readonly evidence: "transport-forgot-handle";
    /** What is known of the remote session: nothing. */
    readonly remote: "unknown";
    constructor(options?: {
        readonly cause?: unknown;
    });
}
/**
 * Is `error` a {@link LocalSessionHandleLostError}? By its code as well as its
 * class, so a second copy of the module (a CJS and an ESM build in one
 * process) is still recognised.
 */
export declare function isLocalSessionHandleLost(error: unknown): error is LocalSessionHandleLostError;
/**
 * Run `work`, giving up after `timeoutMs`.
 *
 * The abandoned promise is deliberately NOT cancelled — there is nothing to
 * cancel a vendor's in-flight `Stop` with, and pretending otherwise would be
 * worse than saying we stopped waiting. The timer is unref'd where the runtime
 * allows it, so a pending teardown can never be the reason a process stays up.
 *
 * Shared, not copied: the MCP client's `terminate()` bounds its wait with it.
 *
 * @throws {@link ToolTeardownTimeoutError} when `timeoutMs` passes first; what
 *   `work` throws, otherwise
 */
export declare function withTimeout(work: () => Promise<void>, timeoutMs: number, what: string): Promise<void>;
//# sourceMappingURL=toolSessions.d.ts.map