UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

134 lines (133 loc) • 8.78 kB
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;