@sema-agent/core
Version:
Stateless, task-oriented AI agent core
821 lines • 59.3 kB
TypeScript
import type { MailboxAppendMessage, MailboxStore } from "./mailbox-store.js";
/** 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 = "completed" | "failed" | "killed" | "cancelled" | "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. */
export declare function steerPriorityOf(x: {
readonly priority?: SystemInjectionPriority;
}): SystemInjectionPriority;
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;
* third mirror: types.ts `BackgroundChildEvent.stoppedBy` — keep all three in sync). 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">;
/** 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;
}