@sema-agent/core
Version:
Stateless, task-oriented AI agent core
134 lines (133 loc) • 8.78 kB
TypeScript
import type { MailboxPeerMeta } from "../core/mailbox-store.js";
import type { CrossSessionHoldCause, CrossSessionInboundVerdict } from "./cross-session-judge.js";
import { type PeerDeliveryReceiptState } from "./peer-notices.js";
/** The `cause` vocabulary of a `peer.inbound_disposition{disposition:"held"}` notice: the parity
* judge's causes plus the drain-stage gate's TRANSIENT `rate_limited` (a bucket-wait, not a review
* hold — it never enters the held queue; the drain stops at that seq and re-judges next pass). */
export type PeerInboundHoldCause = CrossSessionHoldCause | "rate_limited";
export interface PeerHeldEntry {
/** The recipient box seq of the held record (its identity for the review face). */
readonly seq: number;
/** The recipient's box handle. */
readonly box: string;
/** The stored body (`[summary] message`, the canonical stored form). */
readonly content: string;
/** The sender's display name as the record carried it (display only). */
readonly from?: string;
readonly sentAt: number;
readonly hopChain?: readonly string[];
/** The typed record — reply routing (`fromSession`) and the attestation the re-judgment reads. */
readonly peerMeta?: MailboxPeerMeta;
readonly heldAt: number;
/** The cause last announced for this entry (re-judgment may move it). */
cause: CrossSessionHoldCause;
/** Approved by the review face while no run of the session was live: delivered at the next flush
* (a re-judgment that now refuses still wins — CC `l0e`'s approve arm re-checks policy). */
released?: boolean;
}
export type PeerHeldSettlement = "expired" | "delivered" | "refused" | "denied" | "dropped";
export type PeerHeldSettleReason = "evicted" | "deadline" | "shutdown" | "rejudge" | "approved" | "denied" | "cancelled" | "inject_failed" | "superseded";
/** The clock/timer seam (tests inject a virtual one). Handles are opaque. */
export interface PeerClock {
now(): number;
setTimeout(fn: () => void, ms: number): unknown;
clearTimeout(handle: unknown): void;
}
export declare const realPeerClock: PeerClock;
/** The live seats a bound run gives the queue. `inject` is the ONLY member that is run-scoped (a run's
* own injection lane); the others survive the run (the receipt route rides the mailbox, the judge
* reads the deployment's settings getter) and are what a headless deadline settles through. */
export interface PeerHeldQueueSink {
/** Re-judge an entry against the recipient's CURRENT settings and mode class (CC `M`). */
judge: (entry: PeerHeldEntry) => CrossSessionInboundVerdict;
/** Deliver a released entry into the live run's model context. Absent ⇒ no run is live. Throws ⇒
* the lane refused the frame (settled `dropped`, receipt sent). Returns `"deferred"` ⇒ the run is
* live but this pass's delivery budget is spent: the entry stays released and goes at the next
* pass's flush (the same wait a run-less approval takes). */
inject?: (entry: PeerHeldEntry) => void | "deferred";
/** The receipt to the sender (the binding routes it into the sender's box). */
receipt: (entry: PeerHeldEntry, state: PeerDeliveryReceiptState) => void;
/** Announce an entry entering the queue (or its cause changing on re-judgment). */
onHeld?: (entry: PeerHeldEntry, heldCount: number, cause: CrossSessionHoldCause, reannounce: boolean) => void;
/** Announce an entry leaving the queue. */
onSettled?: (entry: PeerHeldEntry, settlement: PeerHeldSettlement, reason: PeerHeldSettleReason, heldCount: number) => void;
}
export interface PeerHeldQueueConfig {
sink: PeerHeldQueueSink;
/** The resolved `dialogExpiry`: ms before a REVIEW-cause hold expires; `null` = never. */
dialogExpiryMs: number | null;
}
export type PeerHeldSettleOutcome = "gone" | "delivered" | "released_pending" | "dropped" | "dropped_by_policy" | "denied" | "expired";
export interface PeerHeldQueue {
/** The registry key (scope + box). */
readonly key: string;
/** Bind (or re-bind) the live seats. Idempotent per run; a later bind replaces an earlier one. */
configure(cfg: PeerHeldQueueConfig): void;
/** The bound run ended: approvals now park as `released`; deadlines and receipts keep working. */
detachLeg(): void;
/** `true` once {@link configure} ran at least once (the queue can route receipts). */
configured(): boolean;
/** Park a message (CC `v` hold arm): evict-oldest when full, arm the deadline for a review cause,
* announce, receipt `held`. */
hold(entry: Omit<PeerHeldEntry, "heldAt" | "released">): void;
/** Re-judge every entry against current settings (CC `D`): released ⇒ delivered (or parked
* `released` with no live run), refused ⇒ refused with a receipt, still held ⇒ re-announced only
* when the cause moved. */
rejudge(): {
released: number;
refused: number;
held: number;
};
/**
* #1036 — the box is this queue's durable seat, so an entry whose record is no longer IN the box was
* settled by whoever held the lease meanwhile (a takeover acked it, delivered or refused). Called by a
* drain right after it claims the lease, with the seqs the claimed snapshot holds (a claim marks EVERY
* unacked record, so absence is absence): such entries leave the queue as `expired` / `superseded`,
* with no receipt (the owner that settled them sent one) and no decision recorded (there is no record
* left to decide about). Returns how many were retired.
*/
reconcile(present: ReadonlySet<number>): number;
/** The review face (CC `l0e`): approve re-judges then delivers (or parks `released` with no live
* run), deny/cancel drop with the matching receipt. `"gone"` when nothing is held under that seq. */
settle(seq: number, decision: "approve" | "deny" | "cancel"): PeerHeldSettleOutcome;
list(): readonly PeerHeldEntry[];
size(): number;
/** CC `HBe` — a graceful shutdown settles everything still held as `expired` with receipts. */
shutdown(): void;
/**
* #1036 — the TERMINAL DECISIONS this queue has taken about records that are still parked in
* the box, seq → the settlement word. Since a held record now stays in its durable box (the box is
* this queue's durable seat), a decision taken BETWEEN runs — a human's deny, a deadline, an
* eviction — has to survive the run whose ledger recorded it, or the session's next run re-judges
* the record from scratch and can deliver a message a person already refused. A decision is kept
* only until a drain that holds the lease acks past it ({@link PeerHeldQueue.forgetDecisions}); it
* is process-level, like the queue itself, and a crash loses it exactly as it loses the queue.
*/
decisions(): ReadonlyMap<number, PeerHeldSettlement>;
/** #1036 — a lease-owning drain acked these seqs out of the box: their decisions are spent. */
forgetDecisions(seqs: Iterable<number>): void;
}
/** #1036 — how many terminal decisions about still-parked records one box's queue remembers
* (see {@link PeerHeldQueue.decisions}). Several times the queue's own cap: the queue bounds what is
* LIVE, this bounds what has been decided and not yet observed as consumed. */
export declare const PEER_HELD_DECISION_MEMORY: number;
export declare function createPeerHeldQueue(key: string, opts?: {
cap?: number;
clock?: PeerClock;
}): PeerHeldQueue;
/** A session's held-queue key: (scope, the session's box) — and, for a queue of ANOTHER box that session
* drains (its scope's principal inbox), that box's handle as a third member: the approval face addresses
* entries by seq, and seqs are minted per box, so one queue never mixes two boxes. */
export declare function peerHeldQueueKey(scope: string, sessionId: string, box?: string): string;
/** The held queue of a session (created on first use) — of its own box, or of `box` (the principal inbox it
* also drains). `clock` is honored only at creation. */
export declare function peerHeldQueueFor(scope: string, sessionId: string, opts?: {
cap?: number;
clock?: PeerClock;
box?: string;
}): PeerHeldQueue;
/** The held queue of a session if one exists in this process (a host review face reads through this
* without creating state for sessions that never held anything); `box` as in {@link peerHeldQueueFor}. */
export declare function peerHeldQueueIfAny(scope: string, sessionId: string, box?: string): PeerHeldQueue | undefined;
/** Test seam: drop every queue (timers cleared, NO receipts — the crash form). Never called by production code. */
export declare function resetPeerHeldQueuesForTests(): void;