import type { RuntimeSelection } from "../selection/runtime-selection.js"; import type { DeliveryOutcome, GlobalAgentMessage, ListedRuntimeObject, RuntimeAddress, RuntimeKind, RuntimeObjectKind, SerializedRuntimeAddress } from "./messaging-contract.js"; /** * WS-15 §6.1's `transport`, which `runtimeKind × objectKind` CANNOT recover (review r1, M1). * * An in-daemon Dispatch/Chat session (`winter-thread`) and a spawned Code session (`winter-session`) * are both `winter-agent`/`session`, and that is precisely the split the Winter messaging adapter * routes on: a direct in-process push versus the spawned session's `Query.messaging` wire facet. */ export type RuntimeTransport = "winter-thread" | "winter-session" | "claude-handle" | "claude-child"; /** * WS-15 §6.1's directory record: one addressable runtime object (a session or one of its children). * * `selection` is the object's OWN persisted runtime, never its parent's — R-7b-1's whole point, and * what makes WS-13c §8 ("the child's own record is authoritative on resume") enforceable rather than * aspirational. `generation` is WS-10 §12's monotonic counter, the thing a `toGeneration` on an * envelope is checked against so a message addressed to a previous incarnation is refused rather * than delivered to its successor. */ export interface RuntimeDirectoryEntry { /** The canonical serialization — the key every other method takes. */ address: SerializedRuntimeAddress; /** The structured form of `address`. Stored alongside so a reader never re-parses. */ parsed: RuntimeAddress; runtimeKind: RuntimeKind; objectKind: RuntimeObjectKind; /** WS-15 §6.1's own field — see `RuntimeTransport`; NOT derivable from the two above. */ transport: RuntimeTransport; /** The display name a `ListAgents` listing shows; absent when the object has never been named. */ displayName?: string; /** WS-15 §6.1's `title` — the session's own title, when it has one. */ title?: string; status: ListedRuntimeObject["status"]; /** `code` | `dispatch` | `chat` — an open string here because the mode vocabulary is the host's (WS-15 §2). */ mode: string; cwd?: string; /** WS-10 §12's incarnation counter. */ generation: number; /** This object's own persisted runtime choice (R-7b-1 / WS-13c §8). */ selection: RuntimeSelection; /** Present for a child; the canonical address of its parent session. */ parentAddress?: SerializedRuntimeAddress; /** The backend (runtime-side) session id, absent while "starting". */ backendSessionId?: string; /** * WS-14 §6 rule 2: the `CLAUDE_CONFIG_DIR` OBSERVED in `SpawnOptions.env` — never the value that was * configured — recorded before the process is returned, because "this is the only supported way to * know the `claude-resume-` staging root, since the default spawner exposes no post-cleanup * lookup". Rule 5: cleared (set back to absent) only after verified cleanup, which is why it is a * field with a lifecycle rather than a write-once one. * * WRITTEN BY Lane A's supervised spawn proxy, before it hands the process back. READ BY Lane B's * `RuntimeDirectory.recover()` (WS-15 §6.4) and by Lane A's own cleanup reconciliation. Absent for * every `winter-agent` object — it is the official branch's staging root, nobody else's. */ configDir?: string; /** * WS-14 §9 / WS-15 §6.4 step 2: PID **plus process start identity**, never a bare PID — an OS * recycles pids, so a bare one turns "is my child still alive?" into a coin flip that occasionally * answers about somebody else's process. * * WRITTEN BY Lane A's spawn proxy alongside `configDir`. READ BY Lane B's `recover()` step 2, which * marks previously live handles `"unavailable"` until process identity revalidates. Absent for * in-daemon (`winter-thread`) objects, which have no child at all. */ processIdentity?: { pid: number; startedAt: string; }; /** * R-7b-11: whether this session's child was allowed to fetch the runtime's REMOTE FEATURE * CONFIGURATION. Absent (and for every `winter-agent` row) means the shipped default, `"deny"`. * * IT IS ON THE ROW BECAUSE THE SURFACE IS NOT DERIVABLE FROM THE VERSION. Two sessions on the same * pinned artifact advertise different tool sets depending on this one answer, so a reader asking * "what could this session do?" cannot answer it from `selection.engineVersion` alone. Written by * the official adapter's record sink at launch, from `OfficialLaunchPlan.remoteConfig`. WS-23: nothing * writes it any more (the official runtime is retired); a row persisted before then may still carry * it, so the field stays readable. */ remoteConfig?: "deny" | "allow"; capabilities: ListedRuntimeObject["capabilities"]; /** * ISO-8601. A DELIBERATE DEPARTURE from WS-15 §6.1's `updatedAt: number`: every other timestamp on * this seam that a human ever reads is ISO (`RuntimeSelection.decidedAt`, the lease records below), * and one record carrying epoch milliseconds while its neighbours carry ISO is the kind of * inconsistency that produces a `new Date(isoString)` bug in a host months later. The epoch-ms * spelling is kept where it is load-bearing for arithmetic — `HeldMessageRecord.heldAt/expiresAt`, * which the mailbox's own expiry compares numerically, matching the runtime's mailbox exactly. */ updatedAt: string; } /** * Per-address delivery cursors (WS-15 §6.4's recovery, WS-17 row 11's "index.db rebuild preserves * runtime mappings, backend IDs, cursors"). * * A cursor is an OPAQUE string to this seam: what it points at is the adapter's business. */ export interface CursorStore { get(address: SerializedRuntimeAddress): Promise; set(address: SerializedRuntimeAddress, cursor: string): Promise; remove(address: SerializedRuntimeAddress): Promise; /** Every cursor, for the restart sweep. */ all(): Promise>; } /** * One held message, as the store keeps it. * * The FULL envelope is stored, not a reference: WS-15 §6.4's restart recovery has to be able to * deliver a message the process was holding when it died, and a record that only remembered an id * would need the message to have been persisted somewhere else as well. */ export interface HeldMessageRecord { messageId: string; /** The canonical address the message is held FOR. */ receiver: SerializedRuntimeAddress; reason: string; /** WS-10 §13: an explicit hold persists; a default-class hold expires (5-minute dialog expiry). */ kind: "default" | "explicit"; /** Epoch milliseconds, matching the runtime's own mailbox. */ heldAt: number; /** Present only for `kind: "default"`. */ expiresAt?: number; message: GlobalAgentMessage; } /** The held-mailbox sink. Caps and expiry are the ROUTER's (see this file's header). */ export interface MailboxStore { listHeld(receiver: SerializedRuntimeAddress): Promise; hold(record: HeldMessageRecord): Promise; takeHeld(receiver: SerializedRuntimeAddress, messageId: string): Promise; clear(receiver: SerializedRuntimeAddress): Promise; /** Every receiver holding at least one message — the entry point of the restart sweep. */ receivers(): Promise; } /** * ONE DELIVERY, from the moment its id exists to the moment its receipt does (WS-15 §6.2). * * The pipeline the router runs is explicit about what must be durable, and in which order: "allocate * and **persist** the request/message ID **before address resolution** (so ambiguous/missing/ * unavailable outcomes are idempotent too) → **persist the envelope** and resolved target generation * → … → **persist the adapter receipt**", so that "a retry returns the stored outcome instead of * starting a second turn". * * `claimedBy` set with no `outcome` is the crash window: WS-15 §6.4 step 5 reconciles exactly those * as `delivery_uncertain`, which is why they are two fields and not one status enum — the pair is the * evidence, and a single "status" would let a writer describe a state it had not actually reached. */ export interface DeliveryRecord { /** WS-10 §12: derived from (sender session, tool-call id), so a retry allocates the SAME id. */ messageId: string; /** The envelope, persisted before resolution — a restart must be able to finish what it started. */ message: GlobalAgentMessage; /** The target generation resolution picked. A later generation is a different incarnation. */ toGeneration: number; /** Set at the atomic claim, before the adapter is invoked; a claim with no receipt is "uncertain". */ claimedBy?: RuntimeKind; /** THE RECEIPT. A retry with the same `messageId` returns this instead of delivering again. */ outcome?: DeliveryOutcome; /** ISO-8601. */ updatedAt: string; } /** The idempotency sink. Policy (dedupe windows, retry limits, loop guards) stays with the router. */ export interface DeliveryRecordStore { get(messageId: string): Promise; put(record: DeliveryRecord): Promise; /** WS-15 §6.4 step 5's entry point: claimed, never receipted — every one is `delivery_uncertain`. */ claimedWithoutReceipt(): Promise; /** * Drop every RECEIPTED record older than this ISO stamp; returns how many went (Lane B fix r1, n3). * * WHY A DURABLE SINK NEEDS A REMOVAL DOOR AT ALL. This store is the only unbounded thing in the * delivery path — the SDK subpath's own in-memory equivalents are explicitly capped * (`rememberBounded`, `MAX_TRACKED_MESSAGE_IDS`) — and on the OFFICIAL branch the growth is worst, * because a send with no tool-call id to derive from writes a record under `no-tool-call:: * ` that can never be looked up again. Without this door a long-lived host's directory * store grows forever with rows that answer no question. * * A CLAIMED-BUT-UNRECEIPTED RECORD IS NEVER PRUNED, at any age. It is WS-15 §6.4 step 5's whole * evidence: the pair (claimed, no receipt) is what makes a crashed delivery `delivery_uncertain` * rather than invisible, and an "old enough" heuristic would silently convert the one state * recovery must report into no state at all. */ prune(receiptedBefore: string): Promise; } /** * A pending `notify_when_idle` (WS-10 §14, WS-15 §6.3). * * §6.3 is explicit that this one "survives restart only when durably stored with valid target * identity/generation" — hence `targetGeneration`, without which a notice could fire for a different * incarnation of the same address. */ export interface IdleSubscriptionRecord { messageId: string; /** Who gets the notice. */ subscriber: SerializedRuntimeAddress; /** What is being watched. */ target: SerializedRuntimeAddress; targetGeneration: number; /** Epoch milliseconds — compared numerically against WS-10 §14's 12-hour expiry. */ createdAt: number; expiresAt: number; } export interface IdleSubscriptionStore { list(): Promise; add(record: IdleSubscriptionRecord): Promise; remove(messageId: string): Promise; } /** * A display-name lease, live or released (WS-10 §11 rule 5, WS-15 §6.4 step 6). * * WHY THE HISTORY OUTLIVES THE ENTRY. Rule 5 says a STALE name is refused — not "not found". Telling * those two apart after the object is gone requires remembering that the name once meant something, * which `RuntimeDirectoryEntry.displayName` cannot do (it disappears with `remove()`). A released * lease is that memory, and it is the only thing that lets the router answer "that name referred to a * session that has since exited" instead of "no such agent". */ export interface NameLeaseRecord { /** The display name, exactly as a model or user would write it. */ name: string; address: SerializedRuntimeAddress; /** The holder's generation at the time of the claim. */ generation: number; /** ISO-8601. */ claimedAt: string; /** ISO-8601; absent while the lease is held. */ releasedAt?: string; } export interface NameLeaseStore { /** Every record for a name — a held one (at most one) plus released ones. Order is the caller's business. */ lookup(name: string): Promise; claim(record: NameLeaseRecord): Promise; release(name: string, address: SerializedRuntimeAddress, releasedAt: string): Promise; /** Every currently-held lease — WS-15 §6.4 step 6 sweeps these by generation. */ held(): Promise; /** * Drop every RELEASED lease released before this ISO stamp; returns how many went (Lane B fix r1, n3). * * A HELD LEASE IS NEVER PRUNED, at any age — it is the live answer to "who owns this name". * * THE RETENTION IS A PRODUCT DECISION, not a cleanup interval: it is exactly how long a stale name * keeps answering "that referred to something that has gone" instead of "no such agent" (WS-10 §11 * rule 5). Prune too eagerly and a model that addresses a session that exited an hour ago gets the * refusal rule 5 exists to avoid; never prune and the memory of every name ever used is permanent. */ prune(releasedBefore: string): Promise; } /** * R-7b-2's seam. * * The plan pins the first five members; `deliveries`, `subscriptions` and `names` were added in fix * round 1 (review r1, I1) because three obligations the plan assigns Lane B — WS-15 §6.2's persisted * id/envelope/claim/receipt, §6.3's restart-surviving idle subscriptions, and §6.4 step 6 / WS-10 §11 * rule 5's name leases — had no durable sink at all, here or in the SDK-side router core (whose * outcome map and notification queue are bounded, explicitly non-durable, in-memory `Map`s). The * router's store is the only durable place they can live. */ export interface RuntimeDirectoryStore { load(): Promise; upsert(e: RuntimeDirectoryEntry): Promise; remove(address: SerializedRuntimeAddress): Promise; cursors: CursorStore; mailboxes: MailboxStore; deliveries: DeliveryRecordStore; subscriptions: IdleSubscriptionStore; names: NameLeaseStore; } /** * The in-memory `RuntimeDirectoryStore` — the default when a host injects none, and the store every * hermetic test uses. * * FULLY IMPLEMENTED, not a stub: it is the test default, so "the directory works" must be provable * without a host. It touches no filesystem, no `~/.winter`, no keychain, and holds nothing across a * process — which is also the honest statement of what a host gets if it never supplies one. */ export declare function createInMemoryRuntimeDirectoryStore(): RuntimeDirectoryStore;