UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

673 lines (625 loc) 33.9 kB
/** * # src/oversight/local.ts — `LocalOversight`, the OSS {@link OversightBackend} (kestrel-telx.4) * * The no-account, no-network, LOCAL PAPER POD implementation of the oversight seam * ({@link ../protocol/oversight.ts}, contract §5.1) over the in-process * {@link ../session/controller.ts SessionController}. The CLI/Ink Rendering (kestrel-jvr4.4) and the * platform's `RemoteOversight` (kestrel-telx.5) are the TWO implementations of one interface; this is * the one that runs with no signup, no broker, and no HTTP — `mode:"paper"`, `operation` ABSENT * (ADR-0035 §b). * * ## THE INVARIANT — a strict VIEW over harness state (contract invariant 1, ADR-0035 §i) * This module **READS** the typed structs the runtime already projects and **NEVER WRITES THE BUS**. * There is exactly one value that crosses into the graded Bus: the `AgentTurn` the * {@link SessionController} derives from the caller's {@link AuthoredResponse} inside * {@link respond}/{@link submitBound}. Concretely: * * - the Book kernel on {@link frame} is `projectKernel(<the kernel of the frame the DRIVER delivered>)` * and NOTHING ELSE — no section is computed here, none is dropped, none is synthesized. The fixture * `tests/oversight.local.test.ts` asserts BYTE-IDENTITY against `projectKernel(harnessKernel)` for a * session driven through the real `start`/`advance`/`submit` path, so computing even one section * locally REDS (the red-on-inert probe); * - this file imports NOTHING from `src/bus/` — no `BusWriter`, no `writeBusFile`, no `serializeBus`. * That is asserted STRUCTURALLY (an import-graph test over this source), because "no write path" is * the kind of property a behavioural test can only sample. * * ## `frameRoot` is the RUNTIME's, never the projection's (contract §3.5) * The runtime delivers a `SessionFrame` to the AGENT and `frameRoot = sha256(canonical Frame)` addresses * THAT. {@link OversightDelivery.kernel} is a VIEW of it. This module copies `frameRoot` through * verbatim from the {@link Delivery}; it never re-hashes the projection. * * ## DETERMINISM — ordinals, never clocks * No wall clock, no RNG. `asofSeq` and every `seq` here is the LOCAL FACT ORDINAL: a monotone counter * over the oversight facts this backend has observed, in drive order (delivery, turn, act, message, * finalized). Same controller + same call sequence ⇒ identical seqs and byte-identical frames. Pre-start * (no facts yet) `asofSeq` is `-1`, the same "nothing yet" idiom as * {@link ../protocol/session.ts OPEN_ORDINAL}. `SpectatorFrame` is the ONE place the contract permits a * wall clock — and this backend never builds one (`spectator: null` always; a local pod has no live * watchlist to be spectating). * * ## What is DELIBERATELY not here (fail closed, never a silent no-op) * - `resolveInput` is NOT implemented in this module. The parse-first resolver + its golden resolution * table is bead kestrel-jvr4.5; wiring a stub here would be a gate whose stated coverage exceeds its * actual coverage. It is a REQUIRED constructor dependency instead — {@link LocalOversightSpec.resolveInput} * — so there is no under-implemented method on the surface. This module only enforces TOTALITY around * it: a resolver that throws is turned into a `parse-error`, never into chat. * - `allocate` / `arm` / `coverage` acts are REFUSED with an explicit reason: a degenerate one-Book * phase-0 pod has no allocation channel and there is no owner arm path (arming is authoring). A * silent `ok:true` no-op would be a lie. They are not {@link NARROWING_ACTS}, so refusing them is * permitted by the contract. * - escalation: one tier runs locally, so every {@link OversightTurn.escalated} is `null` and the * standing `escalation.pending` is `false` — a fact about this backend, not a hidden channel. */ import { jsonHash } from "../canonical/json.ts"; import { sha256 } from "../crypto/sha256.ts"; import type { Kernel } from "../frame/types.ts"; import type { SessionDiagnostic, SessionId } from "../protocol/session.ts"; import { NARROWING_ACTS, OVERSIGHT_TERMINAL_TYPES, type ActReceipt, type AgentTier, type AgentTierStatus, type Authorship, type BookView, type Caller, type InputResolution, type Mode, type OrgNode, type OversightAct, type OversightActRecord, type OversightBackend, type OversightDelivery, type OversightEvent, type OversightFrame, type OversightIdentity, type OversightMessage, type OversightTurn, type PodView, type RiskEnvelope, } from "../protocol/oversight.ts"; import type { Scope } from "../protocol/index.ts"; import type { AuthoredResponse, BoundResponse, Delivery, SessionController, SessionFinalized, SessionResponse, } from "../session/controller.ts"; import { projectKernel } from "./project.ts"; /* ── Spec ──────────────────────────────────────────────────────────────────── */ /** * What a local pod is CONFIGURED with — the facts this backend may state that do not come out of the * harness kernel. Everything else on the Frame is projected, never configured. * * `operation` is intentionally ABSENT from this spec and from every identity this backend answers: * a control-plane operation id is minted by the managed service, and a local pod has no account * (ADR-0035 §b). That makes "no account ⇒ no operation" STRUCTURAL here, not a conditional. */ export interface LocalOversightSpec { /** The in-process progression this backend is a VIEW over. Never forked, never bypassed. */ readonly controller: SessionController; /** Who is calling, resolved ONCE upstream (env-first / TTY-second). Copied through verbatim. */ readonly caller: Caller; /** * The parse-first resolver for the ONE input box (contract §3.8, bead kestrel-jvr4.5). REQUIRED — * this module does not own the resolution table and refuses to ship a stub for it. */ readonly resolveInput: (text: string) => InputResolution; /** The single Book of the phase-0 degenerate pod. */ readonly bookId: string; readonly podId: string; /** Coverage = instruments + THE THESIS FOR WHY (instruments alone are not coverage). */ readonly coverage: { readonly symbols: readonly string[]; readonly thesis: string }; /** * The pod's CONFIGURED allocation. Deliberately NOT defaulted from the Book's projected * `kernel.budgetEnvelope`: the Book's envelope is harness truth and stays visible inside the * projected kernel; folding one into the other would be a synthesis this seam must not perform. * `fund`/`de-fund` acts move `ownerEnvelope` (the only envelope the owner owns) and nothing else. */ readonly podEnvelope: RiskEnvelope; /** WHO authors the turns this backend commits — the tier stamp every action carries from day one. */ readonly authoredBy: Authorship; /** Which tier owns the delivered slots. Phase 0 runs one tier. Default `"strategist"`. */ readonly tier?: AgentTier; /** `"paper"` (default) or `"sim"`. `"live"` is UNREPRESENTABLE: a local pod has no broker. */ readonly mode?: "sim" | "paper"; /** Default `["sim", "paper"]` — what a no-account local pod can actually do. */ readonly scopes?: readonly Scope[]; /** The content-hashed System Profile this cascade runs under, when the caller pins one. */ readonly configId?: string; } /* ── Constants ─────────────────────────────────────────────────────────────── */ /** The owner's stamp on an {@link OversightAct}: the `human` rung of the attribution ladder. `model` * and `version` are `null` BY CONTRACT for a human — absent, never invented. */ const OWNER_AUTHORSHIP: Authorship = { tier: "human", model: null, version: null }; /** "Nothing has happened yet" — the same idiom as `OPEN_ORDINAL`. `asofSeq` reads this before the * first local fact, rather than pretending fact `0` exists. */ const NO_FACTS_SEQ = -1; const TERMINAL = new Set<string>(OVERSIGHT_TERMINAL_TYPES); /** * THE BUILD FENCE for the one refusal this module makes. `allocate`/`arm`/`coverage` may be refused * ONLY because they are not de-risking acts — "a narrowing act is never refused" is a contract rule, * not a local opinion. If any of the three ever joined {@link NARROWING_ACTS}, `_Overlap` stops being * `never` and THIS FILE FAILS TO COMPILE, instead of quietly gating a de-risking act. */ type _RefusedActsAreNotNarrowing< _Overlap extends never = Extract<"allocate" | "arm" | "coverage", (typeof NARROWING_ACTS)[number]>, > = true; /* ── The backend ───────────────────────────────────────────────────────────── */ /** * The OSS {@link OversightBackend}. Mint one with {@link openLocalOversight}, drive it with * {@link LocalOversight.start} → {@link LocalOversight.respond}* → {@link LocalOversight.finalize}. * * `start`/`submitBound`/`finalize` are LOCAL EXTENSIONS beyond the eight contract methods: the contract * describes a backend a client ATTACHES to, so it has no lifecycle verbs; the OSS backend is also the * thing that owns the lifecycle. They are the `SessionController`'s `start`/`submit`/`finalize`, one * layer up — never a second progression. */ export class LocalOversight implements OversightBackend { readonly kind = "local" as const; readonly #spec: LocalOversightSpec; readonly #tier: AgentTier; readonly #mode: Mode; readonly #scopes: readonly Scope[]; /** The monotone LOCAL FACT ORDINAL. Deterministic in the drive order — never a clock. */ #nextSeq = 0; #started = false; #sealed = false; /** The slot awaiting an answer, straight off the driver. `null` ⇒ nothing pending. */ #pending: Delivery | null = null; /** The kernel of the LAST frame the DRIVER delivered — the only kernel this backend ever shows. */ #lastKernel: Kernel | null = null; #ownerEnvelope: number; #lastReframeSeq: number | null = null; #lastWatcherActionSeq: number | null = null; /** * A narrowing act ({@link NARROWING_ACTS}) that arrived with NO slot pending. It is consumed at the * NEXT answered slot, where it REPLACES whatever the caller authored with an explicit stand-down. * Authority only narrows — and a veto that waited politely for the caller's next document would not * be a veto. The substitution is recorded twice (the act, and a `stood-down` turn), never silent. * * BOTH answer paths consume it — {@link respond} AND {@link submitBound}. They are one progression in * two wire shapes; a queue only one of them honours is a veto with a bypass door. * * CONSUMPTION IS CONDITIONAL ON A COMMITTED TURN, on both paths. The queue is cleared only once * {@link #commit} reports `ok` — for EVERY refusal diagnostic the driver can return, not a listed few. * A refused drive committed nothing, so the veto has not taken effect anywhere; clearing the queue * before the driver has spoken is precisely the "admitted then silently dropped" door, and it is worse * than a refusal because the owner was already told `ok:true` by {@link submitAct}. */ #pendingNarrow: { readonly act: OversightAct; readonly reason: string } | null = null; readonly #messages: OversightMessage[] = []; readonly #acts: OversightActRecord[] = []; /** The append-only local event log. The opaque stream cursor is a position IN THIS LOG. */ readonly #log: OversightEvent[] = []; #wake: (() => void) | null = null; #woken: Promise<void> | null = null; constructor(spec: LocalOversightSpec) { this.#spec = spec; this.#tier = spec.tier ?? "strategist"; this.#mode = spec.mode ?? "paper"; this.#scopes = spec.scopes ?? ["sim", "paper"]; this.#ownerEnvelope = spec.podEnvelope.ownerEnvelope; } /* ── contract §5.1 ───────────────────────────────────────────────────────── */ /** Who is watching what, under which authority. `operation` is ABSENT — there is no account. */ identity(): Promise<OversightIdentity> { return Promise.resolve({ caller: this.#spec.caller, sessionId: this.#spec.controller.sessionId as SessionId, mode: this.#mode, scopes: [...this.#scopes], }); } /** * The current view — a PURE PROJECTION of harness state. Reads only recorded facts; drives nothing, * mutates nothing, reads no clock. Same state ⇒ byte-identical frame. */ frame(): Promise<OversightFrame> { return Promise.resolve(this.#frameNow()); } /** * The live stream. `after` is the OPAQUE cursor — the local-fact-log position of the LAST event the * client saw, so the stream resumes STRICTLY AFTER it (`Last-Event-ID` semantics); absent ⇒ the * current snapshot first, then everything appended after it. FAIL CLOSED: an uninterpretable cursor * yields a single `oversight.failed` and ends — never a silent replay from the top. */ async *stream(after?: string): AsyncIterable<OversightEvent> { let next: number; if (after === undefined) { next = this.#log.length; yield { type: "oversight.snapshot", frame: this.#frameNow() }; } else { const parsed = parseCursor(after); if (parsed === null) { yield { type: "oversight.failed", reason: `oversight/local: uninterpretable cursor ${JSON.stringify(after)}` }; return; } next = parsed + 1; } for (;;) { while (next < this.#log.length) { const event = this.#log[next++]!; yield event; if (TERMINAL.has(event.type)) return; } if (this.#sealed) return; await this.#awaitAppend(); } } /** * Parse-first resolution of the one input box. Delegated to the injected resolver (kestrel-jvr4.5); * this wrapper only enforces the contract's TOTALITY: a resolver that throws becomes a `parse-error` * carrying the throw, never a silent fall-through to chat. */ resolveInput(text: string): InputResolution { try { return this.#spec.resolveInput(text); } catch (err) { return { kind: "parse-error", diagnostics: [`oversight/local: resolver threw: ${String(err)}`] }; } } /** * A typed act → a local oversight fact (chat can never reach here — only an `act` resolution can). * * - {@link NARROWING_ACTS} (`de-arm`/`pause`/`veto`, and `de-fund`) are ALWAYS admitted. De-risking * is never gated. `de-arm`/`pause`/`veto` genuinely narrow: they stand the pending slot down * THROUGH THE REAL DRIVER, or — with nothing pending — arm {@link #pendingNarrow} so the next * answered slot stands down. * - `fund` widens and `de-fund` narrows the pod's `ownerEnvelope` (de-fund can only ever lower it). * - `allocate`/`arm`/`coverage` are REFUSED with an explicit reason (see the module header). */ async submitAct(act: OversightAct): Promise<ActReceipt> { const structural = structuralRefusal(act); if (structural !== null) return { ok: false, reason: structural }; switch (act.act) { case "allocate": case "arm": case "coverage": return { ok: false, reason: `oversight/local: '${act.act}' has no channel on a phase-0 degenerate one-Book pod ` + `(allocation is a Pod fact this backend does not own; arming is authoring). Refused, not no-op'd.`, }; case "fund": this.#ownerEnvelope = act.ownerEnvelope; // IN FORCE immediately — the envelope moved. It binds no delivered slot, so `slot` is `null`. return this.#recordAct(act, { state: "bound", slot: null }); case "de-fund": // NARROWING: can only ever lower the envelope, never raise it through the de-risking door. this.#ownerEnvelope = Math.min(this.#ownerEnvelope, act.ownerEnvelope); return this.#recordAct(act, { state: "bound", slot: null }); case "de-arm": case "pause": case "veto": { const reason = narrowReason(act); if (this.#pending !== null && !this.#sealed) { // BOUND NOW: a slot is pending, so this act stands it down THROUGH THE REAL DRIVER. Capture the // slot ordinal BEFORE #commit runs — #commit replaces #pending with the next delivery — so the // receipt names the exact slot the owner just put in force. const slot = this.#pending.ordinal; const receipt = this.#recordAct(act, { state: "bound", slot }); await this.#commit(() => this.#spec.controller.advance({ kind: "stand-down", reason }), OWNER_AUTHORSHIP); return receipt; } // No slot to bind (nothing pending, or the chain is sealed): ADMITTED but not yet in force. It is // QUEUED to stand down the NEXT answered slot (respond/submitBound). The owner is told 'queued', // never a bare `ok:true` that would read as 'the risk is removed NOW'. if (!this.#sealed) this.#pendingNarrow = { act, reason }; return this.#recordAct(act, { state: "queued" }); } } } /** * Post an owner message. CARRIES NO AUTHORITY — it can never arm, size, fund, or place. * Content-addressed, so an immediate re-send of identical bytes is an IDEMPOTENT duplicate and never * a second fact (no new `seq`, no second stream event). */ say(text: string, to: string | null): Promise<{ readonly seq: number; readonly messageId: string }> { const messageId = sha256(jsonHash({ author: "owner", text, to })); const last = this.#messages[this.#messages.length - 1]; if (last !== undefined && last.messageId === messageId) { return Promise.resolve({ seq: last.seq, messageId }); } const message: OversightMessage = { seq: this.#mintSeq(), messageId, author: "owner", to, text }; this.#messages.push(message); this.#append({ type: "oversight.message", message }); return Promise.resolve({ seq: message.seq, messageId }); } /** * Answer the delivered slot — the ONE thing that crosses into the graded Bus. The vocabulary is the * EXISTING {@link AuthoredResponse}; no fourth kind is added. Fails closed with a typed * {@link SessionDiagnostic} (committed state untouched) when nothing is pending, the chain is sealed, * or the driver refuses. * * A QUEUED NARROWING ACT BINDS HERE and REPLACES what the caller authored — but it is CONSUMED ONLY * ON A COMMITTED TURN. A refused drive (`not-pending`, `sealed`, `stale-frame`, or any other * diagnostic) committed nothing, so the veto is still un-landed and stays queued for the next answered * slot. The owner was told `ok:true` when the act was admitted; a driver refusal must never be the * door through which that de-risking act is silently discarded. */ async respond( r: AuthoredResponse, ): Promise<{ readonly ok: true; readonly turn: OversightTurn } | { readonly ok: false; readonly diagnostic: SessionDiagnostic }> { const queued = this.#pendingNarrow; const effective = this.#narrowedTo(queued, r); const result = await this.#commit(() => this.#spec.controller.advance(effective), this.#stampFor(effective, r)); this.#settleNarrow(queued, result.ok); return result; } /* ── local lifecycle extensions (the controller's own verbs, one layer up) ── */ /** START: drive the controller's OPEN and record the delivered slot. Fails closed on a second call. */ async start(): Promise<{ readonly ok: true; readonly delivery: OversightDelivery } | { readonly ok: false; readonly reason: string }> { if (this.#started) return { ok: false, reason: "oversight/local: already started" }; this.#started = true; const delivered = this.#accept(await this.#spec.controller.start()); if (delivered === null) return { ok: false, reason: "oversight/local: the driver delivered no OPEN slot" }; return { ok: true, delivery: delivered }; } /** * The EXPLICIT-BINDING answer (`session/turn`): the caller supplies the full pentad and the driver * reconciles it against the recorded slot — idempotent on a byte-identical re-send, typed refusal on * a conflict. Same view discipline as {@link respond}; the bound path is not a second progression. */ async submitBound( r: BoundResponse, ): Promise<{ readonly ok: true; readonly turn: OversightTurn } | { readonly ok: false; readonly diagnostic: SessionDiagnostic }> { // A QUEUED NARROWING ACT BINDS HERE TOO. `submitBound` is the same progression as `respond`, one // wire-shape over — so it must consume `#pendingNarrow` exactly as `respond` does. Admitting the // caller's authored pentad while a veto waits would (a) ARM the very slot the owner stood down and // (b) leave the stale veto queued to hijack an UNRELATED later slot with a fabricated `tier:"human"` // stand-down. Authority only narrows, and a de-risking act is NEVER admitted-then-dropped. const queued = this.#pendingNarrow; // The queue is aimed at the NEXT ANSWERED SLOT — i.e. the one the driver has PENDING. A bound submit // naming any other ordinal is a re-send of an already-committed turn (the idempotent-reconcile path): // it answers no slot and arms nothing, so it must NOT consume the veto. The veto stays queued for the // slot it is actually aimed at rather than being spent on a replay. if (queued === null || this.#pending === null || this.#pending.ordinal !== r.ordinal) { return this.#commit(() => this.#spec.controller.submit(r), this.#spec.authoredBy); } // Substitute the BODY only: the binding pentad still addresses the slot the owner is standing down. const stoodDown: BoundResponse = { ...r, body: { kind: "stand-down", reason: queued.reason } }; const result = await this.#commit(() => this.#spec.controller.submit(stoodDown), OWNER_AUTHORSHIP); this.#settleNarrow(queued, result.ok); return result; } /** FINALIZE: seal through the SAME Bus/Blotter/Grade path and end the stream. */ async finalize(): Promise<SessionFinalized> { const finalized = await this.#spec.controller.finalize(); this.#pending = null; this.#append({ type: "oversight.finalized", seq: this.#mintSeq(), sessionId: finalized.sessionId, tipHash: finalized.tipHash, // A local pod writes no artifacts: there is no object store to name. Empty is the FACT. artifacts: [], }); this.#sealed = true; this.#wake?.(); return finalized; } /* ── internals ───────────────────────────────────────────────────────────── */ #mintSeq(): number { return this.#nextSeq++; } /** Record a driver {@link Delivery} and emit it as an oversight fact. `null` (settle) clears the slot. */ #accept(d: Delivery | null): OversightDelivery | null { if (d === null) { this.#pending = null; return null; } this.#pending = d; this.#lastKernel = d.frame.kernel; const delivery = this.#deliveryOf(d); this.#append({ type: "oversight.delivery", seq: this.#mintSeq(), delivery }); return delivery; } /** A driver {@link Delivery} → the contract's {@link OversightDelivery}. The four pentad legs are * copied VERBATIM — in particular `frameRoot`, which addresses the RUNTIME's canonical Frame and is * NEVER re-hashed from the projection — and the frame body is replaced by its projection. */ #deliveryOf(d: Delivery): OversightDelivery { return { sessionId: d.sessionId, ordinal: d.ordinal, parentHash: d.parentHash, frameRoot: d.frameRoot, bookId: this.#spec.bookId, tier: this.#tier, kernel: projectKernel(d.frame.kernel), }; } /** The ONE commit path: run a driver verb, record the receipt + the next slot, emit the facts. */ async #commit( drive: () => Promise<SessionResponse>, authoredBy: Authorship, ): Promise<{ readonly ok: true; readonly turn: OversightTurn } | { readonly ok: false; readonly diagnostic: SessionDiagnostic }> { const ordinal = this.#pending?.ordinal ?? NO_FACTS_SEQ; const response = await drive(); if (!response.ok) { this.#append({ type: "oversight.diagnostic", diagnostic: response.diagnostic, ordinal }); return { ok: false, diagnostic: response.diagnostic }; } const seq = this.#mintSeq(); const turn: OversightTurn = { entry: response.receipt.entry, disposition: response.receipt.disposition, authoredBy, // One tier runs locally: there is no escalation channel to raise. A fact, not a hidden field. escalated: null, }; if (this.#tier === "watcher") this.#lastWatcherActionSeq = seq; else if (turn.disposition === "armed" || turn.disposition === "revised") this.#lastReframeSeq = seq; this.#append({ type: "oversight.turn", seq, turn }); this.#accept(response.next); return { ok: true, turn }; } /** What a queued narrowing act TURNS the caller's response into: an explicit stand-down. Pure — it * does NOT clear the queue, because nothing has been committed yet at the point it is called. */ #narrowedTo(queued: { readonly reason: string } | null, r: AuthoredResponse): AuthoredResponse { return queued === null ? r : { kind: "stand-down", reason: queued.reason }; } /** * THE ONE PLACE the narrow queue is cleared — shared by {@link respond} and {@link submitBound} so the * two answer paths cannot drift apart. The queue is spent ONLY when the driver actually COMMITTED the * substituted turn; on ANY refusal it is left in place, so the act the owner was told was admitted is * still live for the next answered slot rather than silently gone. * * The identity check (`=== queued`) means a narrowing act that arrived DURING the awaited drive is * never clobbered by the older act's consumption — a strictly narrowing queue is never widened here. */ #settleNarrow(queued: { readonly act: OversightAct; readonly reason: string } | null, committed: boolean): void { if (queued !== null && committed && this.#pendingNarrow === queued) this.#pendingNarrow = null; } /** A turn the OWNER's narrowing act forced carries the OWNER's stamp, not the agent's. */ #stampFor(effective: AuthoredResponse, authored: AuthoredResponse): Authorship { return effective === authored ? this.#spec.authoredBy : OWNER_AUTHORSHIP; } /** Record the act as a local oversight fact and stamp the RECEIPT's lifecycle: `"queued"` (admitted, * not yet in force — no slot) or `"bound"` (in force now, `slot` = the delivered slot's ordinal, or * `null` for an envelope act that binds no slot). The `actId` addresses the ACT (`{seq, act}`), never * its binding outcome, so it is byte-stable regardless of `state`. */ #recordAct( act: OversightAct, outcome: { readonly state: "queued" } | { readonly state: "bound"; readonly slot: number | null }, ): ActReceipt { const seq = this.#mintSeq(); const actId = sha256(jsonHash({ seq, act })); const record: OversightActRecord = { seq, actId, act, authoredBy: OWNER_AUTHORSHIP }; this.#acts.push(record); this.#append({ type: "oversight.act", record }); return outcome.state === "queued" ? { ok: true, seq, actId, state: "queued" } : { ok: true, seq, actId, state: "bound", slot: outcome.slot }; } #append(event: OversightEvent): void { this.#log.push(event); this.#wake?.(); } /** Park until the next append (or the seal). No timer, no clock — purely append-driven. */ #awaitAppend(): Promise<void> { if (this.#woken === null) { this.#woken = new Promise<void>((resolve) => { this.#wake = () => { this.#wake = null; this.#woken = null; resolve(); }; }); } return this.#woken; } /* ── the projection (pure — this is `frame()`'s whole body) ──────────────── */ #frameNow(): OversightFrame { return { identity: { caller: this.#spec.caller, sessionId: this.#spec.controller.sessionId as SessionId, mode: this.#mode, scopes: [...this.#scopes], }, asofSeq: this.#nextSeq - 1, pod: this.#podNow(), // A local pod has no live watchlist, and `SpectatorFrame.asof` is the contract's ONE wall clock. // This backend never builds one — absent, honestly, rather than a fabricated orientation. spectator: null, pending: this.#pending === null ? null : this.#deliveryOf(this.#pending), messages: this.#messages.map((m) => ({ ...m })), }; } /** The phase-0 pod: a degenerate ONE-BOOK node with an empty `aggregate` (no child has published an * org-fact — absent is UNKNOWN, never a silent `0`). `null` until the driver has delivered a kernel: * a Book with no harness state would have to invent one. */ #podNow(): PodView | null { if (this.#lastKernel === null) return null; const kernel = projectKernel(this.#lastKernel); const book: BookView = { bookId: this.#spec.bookId, coverage: { symbols: [...this.#spec.coverage.symbols], thesis: this.#spec.coverage.thesis }, kernel, tiers: this.#tiersNow(kernel.brief?.hash ?? null), status: { // No wake ⇒ the OPEN briefing, which is not a wake at all: the pill shows the floor severity // and the honest UNKNOWN stays readable at `kernel.wake === null`. severity: kernel.wake?.severity ?? "routine", deadlineMin: kernel.wake?.deadlineMin ?? null, // The local backend does not know the driver's remaining frontier — UNKNOWN, never a guess. wakesRemaining: null, // A local delivery is one vantage; this backend folds no wakes. `0` is the FACT, not a default. coalesced: 0, }, }; const child: OrgNode = { node: "book", book }; return { podId: this.#spec.podId, envelope: { ...this.#spec.podEnvelope, ownerEnvelope: this.#ownerEnvelope }, aggregate: {}, children: [child], }; } #tiersNow(briefHash: string | null): AgentTierStatus { const model = this.#spec.authoredBy.model; return { strategist: { model: this.#tier === "strategist" ? model : null, thesis: this.#spec.coverage.thesis, briefHash, lastReframeSeq: this.#lastReframeSeq, }, watcher: { model: this.#tier === "watcher" ? model : null, lastActionSeq: this.#lastWatcherActionSeq, }, escalation: { pending: false, reason: null, atSeq: null }, configId: this.#spec.configId ?? null, }; } } /** Mint a local backend. Thin by design — the class holds no hidden construction. */ export function openLocalOversight(spec: LocalOversightSpec): LocalOversight { return new LocalOversight(spec); } /* ── helpers (pure) ────────────────────────────────────────────────────────── */ /** The opaque stream cursor: the decimal local-fact-log position the client last saw. `null` ⇒ * uninterpretable (fail closed at the call site — never a silent replay from the top). */ function parseCursor(after: string): number | null { if (!/^\d+$/.test(after)) return null; const n = Number(after); return Number.isSafeInteger(n) ? n : null; } /** The structural floor every act must clear before it is even considered. Returns the refusal reason, * or `null` when the act is well-formed. NARROWING acts clear this floor too — a malformed act is not * a de-risking act, it is noise, and admitting noise is not the same as never gating de-risking. */ function structuralRefusal(act: OversightAct): string | null { const bad = (why: string): string => `oversight/local: malformed '${act.act}' act — ${why}`; switch (act.act) { case "allocate": if (act.target.length === 0) return bad("empty target"); if (!Number.isFinite(act.envelopeR) || act.envelopeR < 0) return bad("envelopeR must be a finite, non-negative number"); return null; case "arm": case "pause": return act.target.length === 0 ? bad("empty target") : null; case "de-arm": case "veto": if (act.target.length === 0) return bad("empty target"); return act.reason.length === 0 ? bad("a de-risking act still states its reason") : null; case "coverage": if (act.bookId.length === 0) return bad("empty bookId"); return act.thesis.length === 0 ? bad("instruments alone are not coverage — a thesis is required") : null; case "fund": if (!Number.isFinite(act.ownerEnvelope) || act.ownerEnvelope < 0) return bad("ownerEnvelope must be a finite, non-negative number"); return act.authorizationRef.length === 0 ? bad("capital authorization requires a signed authorizationRef") : null; case "de-fund": return !Number.isFinite(act.ownerEnvelope) || act.ownerEnvelope < 0 ? bad("ownerEnvelope must be a finite, non-negative number") : null; } } /** The stand-down reason a narrowing act carries into the graded record. */ function narrowReason(act: Extract<OversightAct, { act: "de-arm" | "pause" | "veto" }>): string { return act.act === "pause" ? `owner pause: ${act.target}` : `owner ${act.act}: ${act.target} — ${act.reason}`; }