UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

842 lines • 61.4 kB
import type { MailboxAppendMessage, MailboxStore } from "./mailbox-store.js";
/** The TERMINAL words of {@link TaskNotificationStatus}: the one table every "is this frame a completion?"
 *  reading asks, exported as a value so a consumer outside the package reads these words rather than its own list. */
export declare const TASK_NOTIFICATION_TERMINAL_STATUSES: readonly ["completed", "failed", "killed", "cancelled"];
export type TaskNotificationTerminalStatus = (typeof TASK_NOTIFICATION_TERMINAL_STATUSES)[number];
/** design/135 G2: `"event"` is the ONE non-terminal member — a Monitor batch notification (new stdout
 *  lines from a still-running watch). Everything else is a terminal completion notification. */
export type TaskNotificationStatus = TaskNotificationTerminalStatus | "event";
export type SystemInjectionPriority = "now" | "next" | "later";
/**
 * The outcome an injection entry settles with — what the session did with the frame:
 *  · `queued` — a live lane took it (it rides that run's next turn boundary; accepted, not yet consumed);
 *  · `parked` — no live lane; pended IN PROCESS on {@link PendingSessionNotifications} for the
 *    session's next run in this process (a process restart loses it);
 *  · `parked_durably` — no live lane; appended to the session's DURABLE box
 *    ({@link PendingSessionNotifications.bindSessionBox}) — the next run drains it, in any process;
 *  · `dropped_duplicate` — the session already delivered this exact frame.
 * The park backend is the seat's choice ({@link routeOrParkNotification}), never the producer's.
 */
export type NotificationInjectionOutcome = "queued" | "parked" | "parked_durably" | "dropped_duplicate";
export declare const NOTIFICATION_INJECTION_OUTCOMES: readonly ["queued", "parked", "parked_durably", "dropped_duplicate"];
/** Is `value` one of the injector's outcome words? A host-wired uplink may answer anything (a bare
 *  `void`, an array's `push` count) — such an answer reads as "outcome not reported", never as a word. */
export declare function isNotificationInjectionOutcome(value: unknown): value is NotificationInjectionOutcome;
/** The disposition a park/route decision reports back — the injection outcome minus the dedup fold. */
export type NotificationDisposition = Exclude<NotificationInjectionOutcome, "dropped_duplicate">;
/** The closed value set of {@link SystemInjectionPriority}, as data — the wire-facing entries validate
 *  against THIS rather than against three inline string literals, so widening the type widens the guard.
 *  (Names taken verbatim from CC 2.1.223's `UYo` @ `CC:644918`; see the semantics note on
 *  {@link SystemInjection.priority} for which of CC's behaviors this engine implements.) */
export declare const SYSTEM_INJECTION_PRIORITIES: readonly SystemInjectionPriority[];
/** backlog #389 伴生 (D-3, bad-value loudness) — is this a priority this engine knows at all? A value
 *  outside the set is a caller bug that must be refused, never silently folded into a default: the
 *  no-subscriber retain path indexes `queues[priority]` and the park path stores it verbatim, so an
 *  unknown value used to travel until something downstream failed on it (or didn't, and lied). */
export declare function isSystemInjectionPriority(value: unknown): value is SystemInjectionPriority;
/** The ONE reading of a steer's priority: a parked row or an accepted input that carries none means `"next"`
 *  (the default the live admission has always applied) — so a retry that spells the default and a row parked
 *  without it are the same instruction, whichever side normalized. A tier outside the closed set throws
 *  (`steering.invalid_content`, the sentence names the set); `null` is such a tier, not an absent one. */
export declare function steerPriorityOf(x: {
    readonly priority?: SystemInjectionPriority;
}): SystemInjectionPriority;
/** The two queues a person's input rides on a live run: the `steer` lane (the running turn's next boundary) and the
 *  `followUp` lane (the run's would-otherwise-stop seat). They are also the two families of the stranded-input account — an
 *  input's account is the one of the lane it was queued on (`USER_INPUT_UNDRAINED_FAMILIES`). */
export type UserInputLane = "steer" | "followUp";
/** The ONE reading of "which lane carries this tier": `"later"` rides the follow-up lane (never folded into work in
 *  progress), `"next"` and `"now"` ride the steer lane (`"now"` additionally cuts the running turn). Both injection
 *  seats — the steer verb and the notification lane — branch on it. A parked row that carries no tier reads through
 *  {@link steerPriorityOf} first: `injectionLaneOf(steerPriorityOf(row))`. A tier outside the closed set throws
 *  (`steering.invalid_content`, the same check and sentence as {@link steerPriorityOf}). */
export declare function injectionLaneOf(priority: SystemInjectionPriority): UserInputLane;
export interface TaskNotificationPayload {
    task_id: string;
    /** design/144 §2: `"external"` is CORE-MINTED ONLY — `TaskStream.notify()` casts it onto every
     *  externally injected event, and the input type ({@link ExternalNotificationInput}) deliberately has
     *  no `task_type` field, so an outside producer can never wear an internal lane's type (a caller-declared
     *  "background_bash completed" impersonating a real child). */
    task_type: "background_bash" | "workflow" | "background_agent" | "monitor" | "external";
    toolUseId?: string;
    status: TaskNotificationStatus;
    /** design/135 G2 (Monitor): the batched stdout lines of ONE `status:"event"` notification (lines that
     *  arrived within the same batch window). `result` carries the same text joined/clipped for the model;
     *  this structured mirror is for SDK/UI consumers. */
    lines?: string[];
    /** Per-task monotonically increasing counter. Load-bearing for dedup — {@link taskNotificationDedupKey}
     *  folds it in, so successive notifications of the same task_id+status don't collapse into one queue
     *  entry. Its SEMANTICS are per-task_type (design/144 §1.1):
     *   - `monitor`: the `status:"event"` BATCH counter (design/135 G2) — one per stdout batch window;
     *     terminal notifications omit it (one per status, as before).
     *   - `background_agent`: the STOP-CYCLE number. First settle (spawn stop) = 1; every successful
     *     SendMessage/resume wake bumps it, so the resumed run's settle notifies with 2, 3, … — the
     *     stop→wake→stop second-cycle frame is no longer swallowed by the `task_id:status` dedup key.
     *     The cycle number rides whenever it is KNOWABLE, from either carrier: the retain ledger's
     *     `cycleSeq`, or — on a tier-3 cross-process revival / a parked resume, which deliberately carry
     *     no retain entry — the durable row's own `seq`. ABSENT only when neither exists (retain OFF on a
     *     store-less spawn, an expired ledger with no durable row, the fork lane): the cycle count is not
     *     knowable there, and an honest downgrade to the old single-cycle key beats a caller-declared period
     *     number. So `seq` present does NOT imply a retain entry exists.
     *   - `external`: caller-supplied verbatim ({@link ExternalNotificationInput.seq}) — the injector's
     *     own dedup discriminator across same-task_id+status events.
     *  Consumers (server inbox fold / shell render) key `taskId:status[:seq]` — a frame WITHOUT seq keeps
     *  the old two-part key (single-cycle semantics), by contract. */
    seq?: number;
    /** design/144 §2 (external lane only): WHO sent this event — a deployment-provided attribution label
     *  (e.g. a webhook origin). UNTRUSTED: rendered into the `<task-notification … from="…">` header via
     *  `inlineUntrusted` + attribute-escaping, never as authority. Absent on every internal lane. */
    source?: string;
    /** design/134 §3.3: only on `status:"killed"` — who stopped it. MIRROR of task-registry `StopSource`
     *  (the single source; inlined here to avoid an import cycle — task-registry imports this module);
     *  `BackgroundChildEvent.stoppedBy` is typed by this member and projected from it. OPEN ENUM:
     *  consumers must tolerate unknown values (a future "timeout" etc. is not breaking) and fall back
     *  to default copy. */
    stoppedBy?: "user" | "parent" | "system" | (string & {});
    summary: string;
    /** RB-386① — the failure reason DIGEST on a `status:"failed"` terminal notification
     *  (background_agent lanes): the same bounded text the settle stored as the registry row's `error`
     *  (the completed frame carries the full `result`; the failed frame used to carry NOTHING machine-
     *  readable — consumers had to parse the summary prose, which pre-fix didn't even include the
     *  reason). UNTRUSTED (child/provider-influenceable text) — render-fence before display. Absent on
     *  every non-failed frame and on pre-RB-386 producers. */
    error?: string;
    /** RB-386① — the machine-readable failure code beside {@link error}, when one is known: the child's
     *  `TaskResult.errorCode` taxonomy (brain `[code]` prefixes via extractErrorCode, `limit.*`,
     *  `budget.*`, …). Engine-minted, never model text. Absent when the failure carried no code. */
    errorCode?: string;
    /** Terminal, delegation lanes: the settling run's PROVIDER-BOUNDARY fault assertion, mirrored from
     *  its `TaskResult.apiFailure`. Its PRESENCE is the claim — "this failure came from the transport or
     *  the provider, not from this deployment refusing to send, a limit, or unusable model output" — and
     *  `errorCode` beside it names WHICH terminal; the two answer different questions and neither implies
     *  the other. Members are whatever the failing attempt stated about itself (`status` when the provider
     *  answered with one, `requestId` when it named one), so an EMPTY object is meaningful: "provider
     *  fault, unlabelled", which is not the same statement as absence. Absent on every non-provider
     *  terminal, on every completed run, and on the non-delegation lanes (bash/monitor), whose failures do
     *  not pass through a model provider at all. */
    apiFailure?: {
        status?: number;
        requestId?: string;
    };
    /** Structured exit code of a background command's terminal notification (bash lane): the process
     *  exited on its own with this code. Lets consumers branch on success/failure without parsing the
     *  summary wording. Absent when the process never exited by itself (killed / spawn-failed lanes). */
    exitCode?: number;
    result?: string;
    /** Present (`true`) only on a `status:"killed"` notification that carries a `result` slice: the text
     *  was produced BEFORE the stop won the status race, so it is the child's partial work — the same
     *  semantics as the poll face's `partial_result` flag (one meaning across both surfaces). Absent on
     *  every other notification (a completed result is never flagged). */
    partial?: boolean;
    /** Delegation lanes, terminal frames: the child's EXPLICIT `Agent({name})` spawn key — the same word
     *  its `task_progress.spawnName`, roster row and hook identity carry, rendered as `<agent-name>` so
     *  the parent can `SendMessage({to})` it by name. Minted from the identity the frame's producer holds
     *  (never from the description); ABSENT = an unnamed delegation. */
    spawnName?: string;
    /** Delegation lanes, terminal frames — how the child's report reached this frame under the
     *  hand-back contract: `send` = a report was delivered; `flagged` = delivered under the hand-back
     *  screening's SECURITY WARNING; `withheld` = nothing was delivered (no result text, no partial text).
     *  Absent on non-terminal frames and on the non-delegation lanes. The model face renders a
     *  `<handback>` line only for `flagged` / `withheld` — the parent is told when a report did not
     *  arrive plainly, and nothing for the ordinary case. */
    handback?: HandbackState;
    /** Delegation lanes, terminal frames, present iff the child's leg compacted its context at least once
     *  or opened its compaction breaker: `passes` = landed compactions, `breakerOpen` = compaction was
     *  disabled for the rest of the leg (later context was trimmed, not summarized), `kind` = the last
     *  landed pass's trigger. The parent otherwise cannot tell that a child forgot. */
    compaction?: {
        passes: number;
        breakerOpen: boolean;
        kind?: CompactionKind;
    };
    /**
     * The INBOX NOTICE carrier: one drain pass of this session's box stopped with records still waiting
     * (the pass budget, an admission stop, a lane refusal). `pending` = records still leased and not yet
     * injected when the pass ended; `delivered` = records injected this pass; `urgent` = how many of the
     * waiting ones their sender marked urgent (absent = 0); `box` = the session box. Its PRESENCE is the
     * render discriminator: the model reads ONE engine line (`[Inbox notice] …`), never a shell. Minted
     * ONLY by the engine's session-box drain, keyed `task_id: "inbox:<box>"` with the pass counter as
     * `seq` so it never collides with a message frame's key; absent when the pass emptied the box.
     */
    inboxNotice?: {
        pending: number;
        delivered: number;
        urgent?: number;
        resupplied?: number;
        box: string;
    };
    /** #1036 — THIS occurrence is a RE-SUPPLY of a record the model was already shown once and
     *  whose frame a compaction then summarized away. Same record, same `task_id`, same `seq`, a new
     *  occurrence: `attempt` says which one. ABSENT = not a re-supply (a first delivery); there is no
     *  `false` form. Only the session-box drain mints it. */
    resupplied?: true;
    /** #1036 — which DELIVERY ATTEMPT of this record's frame this occurrence is: 1 = the first
     *  delivery, 2 = the one re-supply a summarized record gets. ABSENT = 1 (there is no `0`, and a
     *  consumer must not read absence as "unknown"). It folds into the injection queue's dedup key
     *  ({@link taskNotificationDedupKey}), which is what keeps a re-supply from collapsing into the
     *  first occurrence's entry. Only the session-box drain mints it. */
    attempt?: number;
    output_file?: string;
    /** Whatever the settling lane knows about its own spend, JSON-rendered verbatim into one `<usage>`
     *  tag. Deliberately untyped — the lanes report different quantities and nothing here should force
     *  one to fabricate a figure it does not have. What IS contracted is the VOCABULARY: a name that two
     *  lanes both publish must mean the same thing on both. The delegation lanes
     *  (`background_agent`, its fork/revive cycles) and the workflow lane therefore agree on
     *  `tokens`/`turns`/`costMicroUsd`/`tool_uses`/`duration_ms`; the workflow lane's fan-out counters
     *  (`agent_count`, `agents_done`, …) and resume counters have no delegation analog and are its own. */
    usage?: unknown;
    /** CC `<diagnostics>` parity: ENGINE-MINTED teaching text for the "result is empty/unexpected —
     *  now what" moment (per-agent read route / journal coordinate / resume command). Producers must mint it
     *  from engine values only (ids, store locators) — never from model-influenceable text. */
    diagnostics?: string;
    /** The child run's SESSION id, when known. A shell keys its fleet/footer rows by its own
     *  local ids; the a* task_id alone can be unmappable on its side — sessionId is the stable cross-system
     *  anchor (the same handle TaskResult.sessionId / fleet rows carry). Optional: bash lane has none. */
    sessionId?: string;
    /** Residual lane A — the child's last ≤10 completed tool steps, so the parent can write a precise resume
     *  prompt for a killed/stopped child without reading the transcript. Rendered as a compact block.
     *  Background-agent lane only; bounded upstream. */
    recentSteps?: import("../agents/subagent-steps.js").SubagentStep[];
    /** Residual lane D (core half) — files the child mutated with edit counts. Background-agent lane only. */
    editedFiles?: import("../agents/subagent-steps.js").SubagentEditedFile[];
    /** Residual lane E — `true` when the child can be revived via SendMessage: a live retained session that
     *  was not killed, OR a NAMED durable row that no USER stop closed (a parent teardown / reap / host
     *  death is what the durable revival lane recovers from). Judged by the revival ladder's own
     *  preconditions, so the claim and the next SendMessage agree. Lets the parent decide
     *  continue-vs-restart without trial-and-error. Background-agent lane only. */
    resumable?: boolean;
    /** A cross-channel correlation key for ONE completion event — the same task's completion fans out
     *  across several independent read faces (this live notification frame, a durable inbox entry, and the
     *  `TaskOutput` poll projection). Minted ONCE per completion, fill-once (first writer wins), on the
     *  same task-registry handle/record every read face serves from — see
     *  {@link "./task-registry.js".UnifiedTaskOutput.completionId} for the shared contract (open-shape
     *  `string`, absent until terminal settle, absent on a pre-P1-3 core build). Deliberately ABSENT on
     *  the `"external"` lane (`TaskStream.notify()`): that lane has its own caller-supplied `seq`
     *  correlation key and no registry-side settle point to mint this from. */
    completionId?: string;
    /** design/176 — the peer-message marker + hop-chain carrier, present ONLY on payloads minted by
     *  the SendMessage delivery legs (the engine's single peer-carrier producer). Three uses: it
     *  carries the forwarding chain (engine-read side channel — never rendered to the model face),
     *  it marks the payload as peer-class for the consumption-boundary chain recording, and it keys
     *  the discipline-block pairing. Deliberately NOT on {@link ExternalNotificationInput}: an
     *  outside caller can neither mint a chain nor wear the peer class. */
    peer?: {
        hopChain: string[];
        /** The SENDING run's session id, when the producer knows it — the reply/receipt address a durable
         *  park of this frame records (`MailboxPeerMeta.fromSession`). Engine-read; never rendered. */
        fromSession?: string;
        /** The sender's admission key (the same token the send-point gate charged), so a durable park
         *  re-judges the frame under the same sender at the drain. Engine-read; never rendered. */
        senderKey?: string;
    };
    /**
     * design/385 §1.4 d1 — the delegated-child → parent UPLINK carrier (`SendMessage("main")`). Its
     * PRESENCE is the render discriminator: the frame reaches the parent's model as a top-level
     * `<agent-message from="…">` user frame (the same-process lane's carrier, distinct from both the
     * `<task-notification>` shell and the cross-session envelope) followed by the peer discipline block.
     * `body` is the child's message as bounded by the producer (no discipline block inside it — the
     * block is frame-adjacent by rule). The classic members (`summary`/`result`) stay filled beside it
     * for wire consumers that project the frame as a card; they are not what the model reads.
     * Minted ONLY by the SendMessage uplink leg (engine-side); an external `notify()` cannot wear it.
     */
    agentMessage?: {
        from: string;
        body: string;
    };
    /**
     * design/385 §4.1 — the CROSS-SESSION carrier (a peer session's message drained from this session's
     * own box). Its PRESENCE is the render discriminator: the model reads a top-level
     * `<cross-session-message from="…" …>` envelope (design/385 §4.2 model-face codec — from-session /
     * from-name / from-mode / from-scope as the typed record carried them; the hop chain never appears)
     * followed by the cross-session discipline block, never a `<task-notification>` shell. The
     * attributes are a PROJECTION of the store record's typed `peerMeta` — the text is presentation,
     * the typed side record ({@link _sema_provenance}) is the authority. Minted ONLY by the engine's
     * session-box drain; an external `notify()` cannot wear it.
     */
    crossSessionMessage?: import("../agents/cross-session-envelope.js").CrossSessionEnvelopeFields & {
        body: string;
    };
    /**
     * design/385 §4.4 / §5.2 (slice 4) — the CROSS-SESSION NOTICE carrier: a delivery receipt about one of
     * THIS session's own outbound messages (held / denied / expired / delivered / refused / dropped) or an
     * idle notice for a `notify_when_idle` ask (idle / exited / unavailable / expired), drained from this
     * session's own box as a notice-kind record. Its PRESENCE is the render discriminator: the model reads
     * the engine-rendered notice line as-is (CC injects `[Cross-session delivery notice] …` /
     * `[Cross-session idle notice] …` as a plain user-frame line) — never a `<task-notification>` shell,
     * never the peer-message envelope (a notice is engine speech about a peer, not a peer's words; the
     * envelope would put it past the parity judgment as a peer's request). `text` is rendered by the
     * drain from the record's TYPED `peerMeta.notice` (the authority); the record's own `content` mirror is
     * never what reaches the model. Minted ONLY by the engine's session-box drain.
     */
    crossSessionNotice?: {
        kind: "delivery_notice" | "idle_notice";
        text: string;
    };
    /**
     * design/385 §1.4 d1 / §4.1 — the engine-minted PROVENANCE side record of an engine-injected peer
     * frame, a typed key (never model text) so a host can attribute and correlate the injection on the
     * wire. Present exactly when {@link agentMessage} OR {@link crossSessionMessage} is — one record
     * per carrier (the notice carrier {@link crossSessionNotice} too), `kind` naming which. Its fields are read per `kind` (see {@link SemaProvenance}):
     * on the agent-message lane `from` is the sender label the frame's attribute spells, `taskId` the
     * sender's run/agent id, `seq` the producer's per-frame counter (= this payload's `seq`) and
     * `agentType` the sender's resolved agent type when the producer knows it; on the cross-session
     * lane `from` is the sender's ADDRESS, `taskId` the recipient's own box handle, `seq` the box seq,
     * and the typed peer record rides in `peerMeta`.
     */
    _sema_provenance?: SemaProvenance;
}
/** design/385 §1.4 d1 / §4.1 — see {@link TaskNotificationPayload._sema_provenance}. `kind` is a
 *  closed set of the two engine lanes that mint the record; a future lane adds a member, never a
 *  second key. */
export interface SemaProvenance {
    /** `agent_message` = the same-process uplink (§1.4 d1); `cross_session_message` = the session-box
     *  drain (§4.1) — `from` is then the sender's ADDRESS, `taskId` the recipient's own box handle,
     *  `seq` the box seq, and the typed peer record rides in `peerMeta`. */
    kind: "agent_message" | "cross_session_message" | "cross_session_notice" | "inbox_notice";
    from: string;
    taskId: string;
    seq: number;
    agentType?: string;
    /** cross_session_message only — the store record's typed peer metadata, verbatim (authority). */
    peerMeta?: import("./mailbox-store.js").MailboxPeerMeta;
}
/**
 * design/144 §2 — the caller-facing input of `TaskStream.notify()`: a STRUCTURED external event to inject
 * into the running task's notification lane (same delivery machinery as background_bash/monitor/agent
 * completions: SystemInjectionQueue dedup → steer/followUp/idle-park three-state delivery).
 *
 * Deliberately a SUBSET of {@link TaskNotificationPayload}: NO `task_type` (core mints `"external"` — a
 * caller can never impersonate an internal lane) and none of the internal-lane fields (lines/stoppedBy/
 * usage/…). Every field is treated as UNTRUSTED data on the model face (the XML renderer sanitizes) —
 * transport, inbound authn, and idle-session durable park are the DEPLOYMENT half (design/43: no
 * daemon/webhook semantics in core; core only takes "a structured event that already reached this
 * process" into THIS run's injection queue).
 */
export interface ExternalNotificationInput {
    /** The caller's correlation id for the event source (free-form; part of the dedup key). */
    task_id: string;
    status: TaskNotificationStatus;
    summary: string;
    result?: string;
    /** Caller-side dedup discriminator: successive events of the same task_id+status need distinct `seq`s
     *  or the queue's dedup key swallows the repeats (same contract as every internal lane). */
    seq?: number;
    /** Attribution label rendered into the `from="…"` header (untrusted; sanitized). */
    source?: string;
}
export interface SystemInjection<TPayload = unknown> {
    kind: "task_notification";
    /**
     * design/373 — the injection ladder is LIVE (all three values carry delivery semantics, the CC
     * 2.1.223 form; the flat "every priority delivers at the next boundary" era ended with this
     * design — its H-1 anchor-correction note is preserved in the design archive, and the ruled
     * changes it named are exactly what landed here):
     *
     *  - `"next"` — the running turn's NEXT boundary (mid-work included), arrival order, consecutive
     *    engine-note frames batch. The pre-373 behavior, byte-identical (the regression baseline).
     *  - `"later"` — never folded into the work in progress: delivered at the run's natural
     *    would-otherwise-stop seat (its own closing turn). A run that never reaches that seat
     *    (abort/maxTurns) re-pends the frame per session — the session's NEXT run delivers it at
     *    turn-open. Structural honesty note: once parked, delivery depends on a next run HAPPENING
     *    (this engine does not own an idle process the way the CC client does) — a recorded
     *    weakening, not a bug.
     *  - `"now"` — `"next"`'s delivery guarantee PLUS the boundary is manufactured early: on the
     *    notification lane the frame takes the queue's class head and the earliest natural boundary
     *    (NO turn interrupt — interrupt authority belongs exclusively to the caller-provenance
     *    steer face, `TaskStream.steer({ priority: "now" })`; the external `notify()` verb REFUSES
     *    `"now"` typed). An unknown value is refused at every entry (bad-value loudness).
     *
     * Park/uplink: the pend store carries the frame's priority ({@link PendingSessionNotifications})
     * and the turn-open batch delivers priority-major (pend order within a class); a record with no
     * carried priority ranks AS `later` and its wire field stays honestly absent.
     */
    priority: SystemInjectionPriority;
    dedupKey: string;
    payload: TPayload;
    /** S2b' — resolved with the ACTUAL injection outcome by the runner's subscribe
     *  listener: "queued" once the boundary steer accepted, "parked" when the idle tail-race (or the
     *  engine-note backlog cap) rejected and the payload was pended per session. Absent =
     *  fire-and-forget producer (no receipt to honor). */
    onDisposition?: (d: NotificationDisposition) => void;
}
/** design/373 (adversarial r3, recorded rule): the key deliberately EXCLUDES the injection tier.
 *  The key names an EVENT's identity; `priority` is delivery metadata about one submission of it.
 *  A repeat under an in-flight key therefore FOLDS regardless of the repeat's tier — the tier of
 *  record is the FIRST accept's, and the folded caller's "queued" receipt is true of that standing
 *  entry. A producer that wants a NEW occurrence delivered under a different tier mints a fresh
 *  `seq` (the documented repeat discipline); silently migrating a queued frame between lanes on a
 *  repeat, or refusing the repeat typed, would each turn a dedup fold into a delivery mutation. */
export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq" | "attempt">): string;
/**
 * The lane-scoped identity of a task, extracted from {@link taskNotificationDedupKey} so every
 * place that treats "same task" as one bucket applies the SAME lane rule.
 *
 * The dedup key had the lane segment from the start; three other sites did not — the pending store's
 * per-task quota, its eviction victim predicate, and its drop ledger key were all the bare `task_id`.
 * The lane segment exists because `task_id` is caller-supplied free-form on the external lane, so an
 * external injector that picks a real child's id landed in the child's quota, evicted the child's frames,
 * and merged into one ledger row carrying ONE `taskType` — the exact impersonation direction this
 * module's header says cannot happen. One rule, one helper, one place to change it.
 */
export declare function taskNotificationLaneKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type">): string;
/** Attribute-context escape for the external `from="…"` header: the value already went through
 *  `inlineUntrusted` (one-line, capped, sanitized), but a `"` would still close the attribute — escape
 *  the XML structural chars so the header can never be broken out of. Exported for the design/171
 *  human-input projection, which renders the same kind of untrusted attribution label — one escape
 *  implementation, never a per-renderer copy. */
export declare function attrEscape(value: string): string;
/** Max rendered length of an untrusted attribution label (the external `from="…"` header and the
 *  design/171 speaker envelope share it — same concern: a display label, not a payload). */
export declare const EXTERNAL_SOURCE_MAX = 120;
/** design/385 §1.4 d1 — the same-process lane's carrier tag (CC `iTe`). */
export declare const AGENT_MESSAGE_TAG = "agent-message";
/**
 * design/385 §1.4 d1 — render the child → parent uplink as CC's same-process form: a top-level
 * `<agent-message from="…">` frame (CC `ZSe`: attribute-escaped sender, nested-tag-neutralized body,
 * no header prose — the attribution IS the attribute) followed by the peer discipline block OUTSIDE
 * the frame (design/176 §3.4 placement: a sender-embedded copy inside the body arrives neutralized,
 * so position distinguishes the real block). The body first passes the harness AUTHORITY-family
 * neutralization ({@link neutralizePeerBody}: every engine envelope a model reads as harness speech —
 * `task-notification`, `user_memory`, `skills`, … — not the reminder tag alone), because the child's
 * text is model output; the pre-carrier `<task-notification>` shell entity-escaped every byte of it,
 * and the carrier form must contain at least as much.
 */
export declare function renderAgentMessageFrame(frame: {
    from: string;
    body: string;
}): string;
/**
 * design/385 §4.1 — render a drained peer-session message as the model-face envelope (the §4.2
 * codec: canonical attribute order, neutralized body) followed by the cross-session discipline block
 * OUTSIDE the envelope. Distinct from {@link renderAgentMessageFrame}: another lane, another trust
 * level, another block — sharing either would blur the classifier's lane judgment.
 */
export declare function renderCrossSessionMessageFrame(frame: NonNullable<TaskNotificationPayload["crossSessionMessage"]>): string;
/**
 * design/385 §4.4 / §5.2 — render a cross-session notice line. The text is engine-authored from typed
 * fields (every untrusted label inside it already passed the lane's label sanitizer); the reminder-tag
 * defusal runs once more here so no carrier can smuggle a system-reminder into the model face.
 */
export declare function renderCrossSessionNoticeLine(notice: NonNullable<TaskNotificationPayload["crossSessionNotice"]>): string;
/** #1036 — the ONE preamble a re-supplied occurrence carries, ahead of whatever carrier it is
 *  riding. It says the true thing (you read this once; the summary that replaced it did not keep it)
 *  without asking the model to reconcile two copies: the first occurrence is gone from the context. */
export declare const RESUPPLIED_FRAME_PREAMBLE = "[Re-supplied] You were shown this message once before; the compaction that replaced older context with a summary did not keep it, so the engine is handing it over again. It is the SAME message, not a new one \u2014 act on it once.";
export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
/** The one sentence a parent reads about a child that compacted: how often, on what trigger, and
 *  whether compaction gave up (after which older context was trimmed away rather than summarized). */
export declare function renderCompactionDisclosure(c: NonNullable<TaskNotificationPayload["compaction"]>): string;
/** The `[Inbox notice]` line — counts only, engine-authored; the records themselves arrive on their
 *  own frames (this pass's) or the next passs', and the pull tool reads them on demand. */
export declare function renderInboxNoticeLine(n: NonNullable<TaskNotificationPayload["inboxNotice"]>): string;
/**
 * The BETWEEN-TURNS pending lane. A task notification born while the session has NO live lane at all
 * (every run of it is over, or the one that is live rejected the injection because its agent was
 * already idle) is parked PER SESSION; the session's next run drains it at start and rides the payloads
 * down the SAME notification lane (steer at the next turn boundary), so in-turn and between-turn events
 * keep one shape. A frame whose session DOES have a live lane never reaches this store —
 * {@link SessionLiveNotificationLanes} takes it to that run's next turn boundary.
 *
 * Bounds (1.254 F4 家族纪律): per-task cap + per-session cap + session-count cap, oldest dropped first —
 * preferring NON-terminal victims (an exit/killed terminal is the single most load-bearing notification of
 * a watch and dies only when nothing else can) — and every drop is counted per task and DISCLOSED on
 * delivery ({@link discloseDroppedPending}), never silent.
 *
 * Dedup (design/144 X4): `pend` drops a payload whose {@link taskNotificationDedupKey} is already parked
 * for the session — the ended-run park leg honors the same key contract as the live queue's enqueue
 * (before this, a repeat external notify() after the run ended would deliver DUPLICATE frames on the next
 * run's drain, violating the notify contract). HONEST WINDOW LIMIT: the dedup set lives and dies with one
 * session's parked batch — it does NOT span the live queue (a key delivered live and then pended after
 * teardown is a fresh domain) and it resets at `drain` (a repeat arriving after the next run drained is
 * deliverable again — same posture as the queue, whose drain also releases keys). A bound-evicted victim
 * releases its key (mirror of SystemInjectionQueue cap eviction), so the set is bounded by the item caps.
 *
 * NOT checkpointed (durable-resume ruling, recorded): the registry itself (`defaultTaskRegistry`) is
 * process-local — a cross-process durable resume cannot reconnect a monitor's watcher/process anyway, so a
 * pending queue that outlives the process would advertise events for handles that no longer exist. Same-
 * process resume legs share this store via the Runner and drain normally.
 */
export declare const MAX_PENDING_EVENTS_PER_TASK = 50;
export declare const MAX_PENDING_EVENTS_PER_SESSION = 200;
export declare const MAX_PENDING_SESSIONS = 100;
/** Session-box bindings are three references and a handle; bounded far above the parked-session cap so
 *  a long-lived leader's binding is not evicted by parked traffic (eviction = the park backend degrades
 *  to in-process, which the receipt then says). */
export declare const MAX_SESSION_BOX_BINDINGS = 4096;
/**
 * A session's DURABLE box, bound to the park seat by the leg that mounts its session-box drain — one
 * mint (the drain mount judgment), two readers (the drain and {@link routeOrParkNotification}), so
 * "a frame parked here is drained there" holds by construction. Absent for a session whose legs mount
 * no drain (a delegated child, an ungrammatical id, no cross-process-safe mailbox).
 */
export interface SessionBoxBinding {
    mailbox: MailboxStore;
    /** The registry/mailbox scope the session's legs mount in (the drain reads the same scope). */
    scope: string;
    /** The box handle (`session.<id>`), minted after the address grammar passed. */
    handle: string;
    /** The session id the box belongs to — the `fromRoot` a durably parked child frame records. */
    sessionId: string;
    /** The deployment's engine-frame floor (`RunnerDeps.sessionBoxEngineFrames`): `true` ⇒ an engine
     *  frame parks in this box as an `engine_frame` record; otherwise it parks in process. */
    engineFrames?: boolean;
    /** Where an append fault is disclosed (the frame then takes the in-process park — never lost). */
    onError?: (e: unknown) => void;
}
/** The hand-back contract's three words (see {@link TaskNotificationPayload.handback}). */
export declare const HANDBACK_STATES: readonly ["send", "flagged", "withheld"];
export type HandbackState = (typeof HANDBACK_STATES)[number];
/** THE hand-back projection, one rule every terminal frame producer calls: a warning ⇒ `flagged`; any
 *  report or partial text ⇒ `send`; neither ⇒ `withheld`. */
export declare function handbackStateOf(facts: {
    result?: string | undefined;
    partialText?: string | undefined;
    warning?: string | undefined;
}): HandbackState;
/** The three-way compaction trigger word a terminal frame carries (the wire's `forced` is `reactive` here). */
export declare const COMPACTION_KINDS: readonly ["manual", "auto", "reactive"];
export type CompactionKind = (typeof COMPACTION_KINDS)[number];
/** The wire trigger word → the frame's kind word (`forced` = a reactive pass the run could not skip). */
export declare function compactionKindOf(trigger: "auto" | "manual" | "forced"): CompactionKind;
/**
 * THE projection every delegation terminal frame calls for its identity + hand-back + context facets:
 * the spawn key from the identity the producer holds (never the description), the hand-back state from
 * the delivered text and the screening's warning, and the child's compaction record verbatim. Absent
 * inputs stay absent; `handback` is always minted (a terminal always says how the report reached it).
 */
export declare function terminalFrameFacets(facts: {
    spawnName?: string | undefined;
    result?: string | undefined;
    partialText?: string | undefined;
    warning?: string | undefined;
    compaction?: TaskNotificationPayload["compaction"] | undefined;
}): Pick<TaskNotificationPayload, "spawnName" | "handback" | "compaction">;
/**
 * A delegated child's report as its completion frame carries it — the one value the parent's notification and the fleet
 * terminal projected from that frame both read: redacted BEFORE the cut, with the pass the task row's copy uses (a token
 * across the cut is never half-shown), then cut with an honest total. Absent in ⇒ absent out.
 */
export declare function notifyResultField(result: string | undefined): string | undefined;
/** The codec version this engine writes into an `engine_frame` record and the highest it reads. */
export declare const ENGINE_FRAME_CODEC_VERSION = 1;
/** The engine's own lanes — the only frames the durable park may encode (an `external` event never is). */
export declare const ENGINE_FRAME_TASK_TYPES: readonly ["background_agent", "monitor", "workflow"];
/** May this frame take the durable engine-frame park? Its lane must be one of the engine's own and it
 *  must wear no shell-less carrier. */
export declare function isEngineFrameParkable(payload: TaskNotificationPayload): boolean;
/** The record text of a durably parked engine frame: the codec version, the injection tier it was
 *  submitted at, and the frame itself (its own `seq` is the lane's cycle count — a different domain
 *  from the box seq, carried inside untouched). */
export declare function encodeEngineFrame(payload: TaskNotificationPayload, priority: SystemInjectionPriority): string;
export type EngineFrameDecode = {
    ok: true;
    payload: TaskNotificationPayload;
    priority: SystemInjectionPriority;
}
/** A version this engine does not read: the record is a NEWER engine's; leave it for one. */
 | {
    ok: false;
    reason: "newer";
    codecVersion: number;
}
/** Not this codec at all (or a frame outside the engine's lanes): settle it without delivery. */
 | {
    ok: false;
    reason: "malformed";
    detail: string;
};
/** Read an `engine_frame` record back. Every field the lane reads is re-typed here; a carrier the
 *  writer must never have encoded is dropped rather than trusted. */
export declare function decodeEngineFrame(content: string): EngineFrameDecode;
/** The durable form of an engine frame: the codec text under `kind: "engine_frame"`, stamped with
 *  the box owner as `fromRoot` (the park seat's own fact, as for a peer-words record); no hop chain —
 *  it is not a peer message and is never admitted as one. */
export declare function engineFrameRecordOf(payload: TaskNotificationPayload, priority: SystemInjectionPriority, box: Pick<SessionBoxBinding, "sessionId">): MailboxAppendMessage;
/**
 * RB-375 A2 (ruled 2026-08-03) — is this frame the TERMINAL of a delegated background agent?
 *
 * It is the one notification class whose audience is the whole DELEGATION TREE rather than the
 * session that produced it: the frame answers "the work you delegated is over", and the party that
 * asked for the work is the conversation at the root of the tree, not necessarily the run that
 * happened to issue the spawn call. Every other class (a shell completion, a Monitor batch, a
 * SendMessage delivery, an external `notify()` event) is addressed to THIS session and stays there.
 *
 * Reuses {@link TERMINAL_STATUSES} — the same terminal-vs-event rule the pending store's eviction
 * preference applies, so "terminal" can never mean two things in this module.
 */
export declare function isDelegatedAgentTerminal(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
/**
 * design/373 (#445 saturation prerequisite) — is this frame a TERMINAL notification (any lane)?
 * Reuses {@link TERMINAL_STATUSES} — the same terminal-vs-event rule the pending store's eviction
 * preference applies, so "terminal" can never mean two things in this module. The runner marks
 * INTERNAL-lane terminal frames cap-preferred at the delivery queue's mouth: a watcher's event
 * storm may delay its own batches, never crowd a completion out of the run waiting on it. The
 * external lane never gets the preference (its `status` is caller-supplied — an untrusted injector
 * must not be able to claim the internal completions' reservation); this predicate itself stays
 * lane-blind, the trust cut is the marking site's.
 */
export declare function isTerminalTaskNotification(n: Pick<TaskNotificationPayload, "status">): boolean;
export interface DrainedPendingNotifications {
    /** Payloads still held when the session's next run drained. design/373 §3.5: batch order is
     *  PRIORITY-MAJOR (now < next < later; a record with no carried priority ranks AS later —
     *  the conservative seat), pend order (chronological) within a class — the idle-dequeue form. */
    items: TaskNotificationPayload[];
    /** design/373 §3.5 — the parked priority per item, keyed by payload identity, present exactly for
     *  records whose pend CARRIED one. A missing key is a fact (a tier-unknown park — e.g.
     *  the harness's undrained sweep hands payloads back without their lane's priority): the wire
     *  field stays honestly absent rather than fabricating `later`. */
    priorities?: Map<TaskNotificationPayload, SystemInjectionPriority>;
    /** task_id → notifications evicted by the bounds while pending (never delivered). `taskType` remembers
     *  the victim's lane so a survivors-none disclosure can still render an honest synthetic payload. */
    /** Keyed by {@link taskNotificationLaneKey}, NOT the bare `task_id` — the external lane's ids are
     *  caller-supplied, so one bare-id row could cover two lanes and name only one of their types. `taskId`
     *  carries the victim's real id back out (the lane key is process-internal and must never be rendered). */
    dropped: Map<string, {
        count: number;
        taskType: TaskNotificationPayload["task_type"];
        taskId: string;
    }>;
    /**
     * Notifications lost when this whole SESSION was evicted by the
     * session-count cap, kept OUT of `dropped`. RB-91 folded them in under a `"(evicted-session)"` string
     * key, which put a session-level fact into a per-task ledger and broke twice for it: the key could
     * collide with a real, free-form external `task_id` (merging two unrelated counts and mislabelling the
     * lane — design/144 X1 already ruled that the external lane needs its own key space), and the
     * survivors-none renderer told the model to call `TaskOutput("(evicted-session)")`, an instruction with
     * nothing behind it. The count is a session fact; it now has a session field.
     */
    droppedWholeSession?: number;
}
/**
 * One run's seat in {@link SessionLiveNotificationLanes}: hand this payload to that run's live
 * injection queue. Four answers:
 *  · `"taken"` — newly enqueued on that run's lane; `onDisposition` (when supplied) is settled by the
 *    queue with the real outcome. Nothing is DELIVERED yet: the boundary can still refuse it;
 *  · `"pending"` — that lane already holds this exact key IN FLIGHT. `onDisposition` is NOT settled
 *    here: the in-flight entry owns the OUTCOME and this copy is no delivery;
 *  · `"duplicate"` — that stream has already DELIVERED this exact frame: nothing to enqueue, nothing
 *    to park; `onDisposition` is NOT settled, the word belongs to the caller;
 *  · `"unavailable"` — the lane can no longer take anything; the caller tries the next lane, and
 *    finally the parked store.
 */
export type LiveNotificationLane = (payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d: NotificationDisposition) => void) => "taken" | "pending" | "duplicate" | "unavailable";
/** One run's seat: its two entry points, plus the two bookkeeping verbs its owner drives. */
export interface LiveNotificationLaneSeat {
    /**
     * The entry for THIS run's OWN producers. It accepts whenever the lane is live: the frame is
     * enqueued (so the queue's own dedup folds a retry), the wire event goes out, and if the agent turns
     * out to be idle the steer's rejection parks it. No route-specific condition is allowed onto it.
     */
    readonly take: LiveNotificationLane;
    /**
     * The entry {@link SessionLiveNotificationLanes} holds (stable identity for register/withdraw) — a
     * frame ANOTHER run of the session is offering. It additionally requires that this lane can deliver
     * at this instant: accepting a foreign frame and then refusing it asynchronously strands it behind
     * this run's own parked-store intake, whereas declining hands it straight back to the router.
     */
    readonly takeAsDestination: LiveNotificationLane;
    /**
     * Record that this lane HANDED this frame to the session route: it accepted the frame and then
     * could not deliver it (its agent was already idle). The key must never come back — the queue's
     * dedup set keeps it, so a second enqueue would be reported as ACCEPTED while nothing delivers or
     * parks it. Remembered per LANE rather than per delivery, so a route of any length terminates on the
     * parked store. A lane that is no longer live records NOTHING (it already answers "unavailable" by
     * liveness). What IS remembered is bounded exactly like the queue's own dedup set — same cap, same
     * insertion-order eviction, and an evicted key is as re-deliverable here as it is there.
     */
    handOff(payload: TaskNotificationPayload): void;
    /** Drop that memory when the lane stops; every later answer is "unavailable" by liveness anyway. */
    release(): void;
}
/**
 * Build one run's lane seat over its injection queue: the SINGLE enqueue site a frame reaches,
 * whether the run produced it or another run of the same session routed it here — so the tier
 * routing, the dedup key and the already-delivered fold cannot fork between the two arrivals.
 */
export declare function createLiveNotificationLane(seat: {
    /** The run's injection queue; its subscriber is what routes an accepted frame by tier. */
    queue: SystemInjectionQueue;
    /**
     * Is the agent in a turn, so the boundary a frame would ride actually exists? Read LIVE, and read
     * ONLY by {@link LiveNotificationLaneSeat.takeAsDestination}.
     */
    canTakeNow: () => boolean;
    /**
     * Has this lane NOT been torn down? The gate for this run's own producers, and the gate on
     * remembering a handoff: a lane merely BETWEEN turns may take again in a moment, so what it handed
     * away must still be remembered; a lane that is over will never take anything.
     */
    isLive: () => boolean;
    /** Keys this run's stream already delivered at turn-open (the cross-layer double-park fold). */
    alreadyDelivered: (dedupKey: string) => boolean;
}): LiveNotificationLaneSeat;
/**
 * The per-session LIVE LANE registry — the destination half of {@link PendingSessionNotifications}.
 * A run registers its injection lane once its session anchor is bound and withdraws it the moment the
 * lane stops accepting (a clean end, a throw, an abort — the run's own teardown), so an entry's
 * lifetime is exactly the lane's liveness.
 *
 * The key is the session the producing lane is ANCHORED to, so this is never a cross-session route: a
 * peer session's live run is not a destination, and a delegated child's own session is not its
 * parent's. A session with no live lane keeps the parked form unchanged.
 *
 * Several lanes may sit under one key: the NEWEST registration is tried first — "the session's current
 * run" — and every lane is tried once, so a lane that has stopped, or that has already handed this very
 * frame away, is passed over rather than swallowing it. A lane that refused a frame therefore cannot be
 * reached by it again, which is what makes a route of any length terminate on the parked store.
 */
export declare class SessionLiveNotificationLanes {
    private readonly lanes;
    register(sessionId: string, lane: LiveNotificationLane): void;
    /** Idempotent (a lane withdraws at its stop AND again at its outermost teardown). The key is dropped
     *  with its last lane so an idle process holds no per-session entry. */
    unregister(sessionId: string, lane: LiveNotificationLane): void;
    /**
     * Offer the payload to the session's live lanes, newest first. The caller's OWN lane is not
     * special-cased: a lane that just refused this frame reports `"unavailable"` for it from then on.
     */
    deliver(sessionId: string, payload: TaskNotificationPayload, priority: SystemInjectionPriority, opts?: {
        onDisposition?: (d: NotificationDisposition) => void;
    }): "taken" | "pending" | "duplicate" | "unavailable";
    /** Sessions holding at least one live lane (observability + the tests' rollback detector). */
    get size(): number;
}
export declare class PendingSessionNotifications {
    private readonly sessions;
    /**
     * The LIVE half of the same question this store answers. Together they are one seat — "where does a
     * notification addressed to session S go?" — with two answers: the session's live lane when it has
     * one, this parked queue when it does not. One object, one lifetime, one key (the session id).
     */
    readonly liveLanes: SessionLiveNotificationLanes;
    /** Whole sessions evicted by {@link MAX_PENDING_SESSIONS} (their pendings were never delivered). */
    droppedSessions: number;
    /**
     * A TOMBSTONE per whole-session eviction, so that session's next drain
     * can still say what it lost. The two item-level caps disclose their drops per task (they mutate a
     * surviving session's `dropped` map), but the session-count cap deleted the whole record and only
     * bumped a global, unattributed counter — from the evicted session's point of view its next drain
     * returned `undefined`, which is exactly what "nothing ever happened" looks like. The class contract
     * says every drop is disclosed on delivery and never silent; this arm was the exception.
     * Bounded like the sessions map itself (a tombstone is two numbers and a lane tag).
     */
    private readonly evictedSessions;
    /** Whole-session losses whose tombstone was itself evicted — the count survives, the attribution
     *  does not. Surfaced on the next session-level disclosure so it is never simply forgotten. */
    private unattributedDrops;
    /** The durable park backend per session ({@link SessionBoxBinding}); insertion-ordered, bounded. */
    private readonly sessionBoxes;
    /**
     * Bind (or, with `undefined`, unbind) the session's durable box. Written at EVERY drain mount of a
     * session's leg, so the binding tracks the deployment's current seats (a mailbox swapped out
     * unbinds at the next mount). A re-bind refreshes the entry's LRU position.
     */
    bindSessionBox(sessionId: string, box: SessionBoxBinding | undefined): void;
    /** The session's durable box, when a leg of it mounted the drain in this process. */
    sessionBoxFor(sessionId: string): SessionBoxBinding | undefined;
    pend(sessionId: string, n: TaskNotificationPayload, priority?: SystemInjectionPriority): void;
    /** Remove and return the session's pendings (one-shot — the next run consumes them exactly once).
     *  design/373 §3.5: the batch comes out PRIORITY-MAJOR (stable sort — pend order within a class;
     *  a no-priority record ranks as later), the idle-dequeue min-value-first form. */
    drain(sessionId: string): DrainedPendingNotifications | undefined;
    /**
     * Keys a live lane of this session has already put in front of a model (the delivered half of the
     * ledger below). Kept beside the parked keys because the WINDOW is the session's, not any one run's:
     * the destination run's own queue dedup dies with it. Released at {@link drain}, exactly like the
     * parked keys. Bounded like everything else here; an evicted key only ever costs a duplicate in the
     * extreme, never a loss.
     */
    private readonly routed;
    /**
     * Does this session ALREADY own this exact frame — parked, or routed onto a live lane? Only one of
     * the two may hold a key: a producer retrying a frame the session has already taken must FOLD rather
     * than be handed to whichever run has since gone live, which would deliver it there and again at the
     * next drain. Callers ask BEFORE routing and BEFORE parking.
     */
    holdsKey(sessionId: string, dedupKey: string): boolean;
    /** Keys the session's DURABLE box took ({@link routeOrParkNotification}'s durable arm) — the third holder
     *  {@link holdsKey} consults, so a retry of a frame the box already holds folds exactly as one the
     *  in-process store holds. Bounded like {@link routed}; a key is released when the append faulted and
     *  the frame took the in-process park instead (that store then owns it). */
    private readonly durable;
    noteDurablePark(sessionId: string, dedupKey: string): void;
    forgetDurablePark(sessionId: string, dedupKey: string): void;
    /** Record that this session has DELIVERED this frame — see {@link routed}. Written at ONE place, the
     *  harness's consumption sink (the step where a queued frame enters the model's input), so the record
     *  can never run ahead of the delivery it claims. */
    markDelivered(sessionId: string, dedupKey: string): void;
    get size(): number;
}
/**
 * The durable box appends ONE run leg caused — its obligations. Attribution is by execution: an append
 * belongs to the leg whose execution (its tools, its producers, its teardown reap, a child terminal it
 * hands up the delegation chain) reached the park seat, whichever lane's routing carried it there. A
 * leg's teardown awaits its own obligations and nobody else's — another session's pending append is
 * never a condition of this leg's completion. A park reached outside any leg is nobody's obligation.
 */
export declare class DurableAppendObligations {
    private static readonly scope;
    private readonly inFlight;
    /** The obligations of the leg whose execution is running now; undefined outside every leg. */
    static current(): DurableAppendObligations | undefined;
    /** Run `body` as this leg (synchronous: the body's own return value, no added tick). */
    run<R>(body: () => R): R;
    track(p: Promise<unknown>): void;
    /** Resolves when every append this leg caused has settled — the fault arm already re-parked in process. */
    settle(): Promise<void>;
}
/**
 * WHERE A FRAME ADDRESSED TO ONE SESSION GOES — the single decision, so the two addresses that ask it
 * (a run's own session, and the delegation tree's root anchor) cannot drift apart. Three outcomes:
 *  · the session ALREADY OWNS the key (parked, or routed onto a live lane) ⇒ fold — neither route it
 *    nor park a second copy; `"parked"` is the word a folded repeat has always been given;
 *  · a live lane takes it ⇒ it rides that run's next turn boundary. This decision records NOTHING in
 *    the ledger: a destination that ACCEPTED a frame can still refuse it at its boundary and park it
 *    then, so only the lane that actually delivers writes ownership
 *    ({@link LiveNotificationLaneSeat.markDelivered});
 *  · nothing can take it ⇒ park, carrying the tier, for the session's next run.
 */
export declare function routeOrParkNotification(store: PendingSessionNotifications, sessionId: string, payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d: NotificationDisposition) => void): void;
/**
 * The durable form of a parked peer-words frame: the SAME `peer_message` record the cross-session lane
 * appends (the recipient's drain renders it through the one envelope and judges it through the one
 * admission gate), with `fromRoot` = the box owner's own session — a frame reaches this seat only
 * through the injector its owner handed to a child of its tree, so the same-tree fact is the seat's to
 * state, not the producer's to claim.
 */
export declare function sessionBoxRecordOf(payload: TaskNotificationPayload & {
    agentMessage: {
        from: string;
        body: string;
    };
}, box: Pick<SessionBoxBinding, "sessionId">): MailboxAppendMessage;
/**
 * Delivery-side overflow disclosure: fold the per-task drop counts into the drained payloads. Two
 * carrier shapes, chosen per survivor:
 *  · a CLASSIC survivor (renders through the `<task-notification>` shell) is annotated IN PLACE — the
 *    first surviving payload of a task that lost events gets a `[task_id]`-prefixed disclosure line
 *    prepended to its summary (backgroundTasks 同规: the id keeps the loss addressable via TaskOutput);
 *  · an AGENT-MESSAGE survivor (design/385 §1.4 d1; renders as the `<agent-message>` carrier, its
 *    `summary` wire-only) is left byte-unchanged, and the disclosure rides as a SEPARATE engine-authored
 *    `event` payload inserted immediately AHEAD of it (engine speech never goes inside a peer frame;
 *    peer speech never goes inside the authority shell that line renders through).
 * The returned array is therefore NOT a 1:1 image of `drained.items`: it can be longer (one inserted
 * line per disclosed agent-message lane, plus the per-lane "nothing survived" lines and the
 * whole-session line below) — consume it by iteration, never by index-pairing against `items`. The
 * zero-disclosure path returns `drained.items` itself (identity, no allocation). A task whose EVERY
 * pending item was evicted still gets one honest synthetic `event` payload saying so — a fully silent
 * loss is never allowed.
 */
export declare function discloseDroppedPending(drained: DrainedPendingNotifications): TaskNotificationPayload[];
export declare class SystemInjectionQueue<TPayload = TaskNotificationPayload> {
    private readonly queues;
    private readonly dedup;
    private readonly listeners;
    /** F4 observability: how many queued items were evicted by the retention cap (never delivered). */
    droppedOldest: number;
    enqueue(item: SystemInjection<TPayload>): boolean;
    drain(priority?: SystemInjectionPriority): Array<SystemInjection<TPayload>>;
    subscribe(listener: (item: SystemInjection<TPayload>) => void): () => void;
    get size(): number;
}