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.
790 lines (758 loc) • 147 kB
text/typescript
/**
* # session/simulate — the frozen-at-wake Simulate driver (the Agent seam's companion to `./day.ts`)
*
* The **wake-driven, agent-in-loop** driver (RUNTIME §7). Where {@link import("./day.ts").runDaySession}
* pauses at each wake and hands an external author a Frame through a FILE handshake, this drives the SAME
* {@link SessionCore} — the same bus-stepping, the same {@link import("../engine").Gate}, the same
* fills-before-sweep ordering — but hands the frozen vantage to the {@link Agent} seam IN-PROCESS and
* consumes the {@link AgentTurn} it returns. Between wakes the standing Plans fire autonomously
* (fire-then-inform); at a wake the driver freezes market time, assembles a date-blind {@link ActingFrame},
* `await`s `agent.decide(frame)`, and serializes every returned {@link Action} onto the graded Bus.
*
* ## Where the determinism boundary sits (RUNTIME §0, the whole point)
* The single value that crosses the boundary is the returned {@link AgentTurn}; BELOW it everything is a
* pure function of Bus bytes (ADR-0011). Every Action is stamped at the **injected wake ts** (no wall
* clock, no unseeded RNG on the record path), so `project(gradedBus)` re-derives a byte-identical Blotter
* and a captured run re-runs as a Backtest through {@link recordedAgent}. The delivered {@link ActingFrame}
* (Rendering, model call, sampling) sits ABOVE the line — the recorded/fixed adapters ignore it entirely.
*
* ## Action → Bus routing (fail-closed, bounded-risk)
* - `supersede` → parse the Kestrel document; the FIRST is the initial arm (no CONTROL, `day.ts` plans-0
* parity), a later one emits a CONTROL `supersede` + de-arm + arm. A parse escape de-arms
* clean with a logged reason (never a crash — RUNTIME §8).
* - `standDown` → a CONTROL `disarm` + de-arm clean (inventory rides its TP/EXIT — never liquidates).
* - `journal` → a **pre-hoc** JOURNAL `author` record, emitted BEFORE the turn's other actions (provably
* pre-hoc by `seq`).
* - `scheduleWake` → recorded as a WAKE `scheduled` record AND unioned onto the agent-driven wake
* frontier (kestrel-5zl.4): the agent's own cadence drives, floored (never replaced) by
* the staleness backstop + optional structural cadence, coalesced/downgraded past the
* attention budget (the squelch stays visible — never a silent drop).
* - `placeOrder` / `cancelOrder` → routed through the **SAME Gate** a fired Plan uses (`core.gate`): an
* order is expressed as a one-shot Plan and armed, so it crosses the identical
* engine→Gate→fill path — SELL≥intrinsic, the budget clamp, and never-naked are enforced by
* the engine, never re-implemented here (bounded risk by construction).
*
* Scope: this is the tracer-bullet slice — the thinnest deterministic Simulate path exercised by
* {@link fixedPlanAgent} + {@link recordedAgent}. The live adapter body, the config matrix, and
* wire-capture are later slices; the full Wake-grammar authoring (subscriptions/Views, the budget
* algebra, per-session structural re-anchoring) is the b3l Wake-router epic — see `armStaleness`/
* `resolveWakeAt` for the conservative defaults taken here.
*/
import type { BusEvent, MetaEvent } from "../bus/index.ts";
import { readBus } from "../bus/index.ts";
import type { Right } from "../bus/types.ts";
import { intrinsic } from "../fair/index.ts";
import { assertNever, parse, print, type KestrelNode, type Module, type PlanStatement } from "../lang/index.ts";
import { armedPlanTermsOf, ENFORCED_PLAN_STATES } from "./armed-plan-terms.ts";
import { decisionControlFields, driverMeasurementVersions, type DecisionEmit } from "../grade/index.ts";
import type { InstrumentSpec } from "../engine/index.ts";
import type { EpisodeSigmoidParams } from "../fill/index.ts";
import { heldPositionAsOfGradedBus } from "../fill/index.ts";
import type { Blotter } from "../blotter/index.ts";
import { project } from "../blotter/index.ts";
import type {
BriefingInput,
EngineAction,
FillRecord,
FramePhase,
FrameRight,
InstrumentSpec as FrameInstrumentSpec,
Kernel,
Mandate,
Brief,
PlanStateEntry,
Position,
PriorSessionTape,
PriorVantage,
SizingHeadroom,
RestingOrder,
VehicleHealth,
WakeRef,
} from "../frame/types.ts";
import { expiryTauYears, etMinuteOfDay, etWallClockMs, type FairTauProvider } from "./clock.ts";
import { SessionCore, busSha256Of, fidelityOf, requireMeta, resolveSpecs, type FillModelName, type SessionReport } from "./sim.ts";
import {
authorCutoffTs,
briefingSnapshot,
computeWakes,
projectAuthorFrame,
scanDateLeak,
snapshotOf,
type AuthorFrame,
type TimeOfDay,
} from "./day.ts";
import {
OPEN_WAKE_KEY,
assertHandlerResponseTimeless,
wakeKeyOf,
type Action,
type ActingFrame,
type Agent,
type AgentConfig,
type AgentTurn,
type CapturedTurn,
type CapturedTurns,
type PlaceOrderAction,
type WakeAt,
type WakeKey,
type WakeSource,
} from "./agent.ts";
import { resolveView, windowServableBy, paneById, atmStraddleExtrinsic, type ViewSelection } from "../frame/pane-catalog.ts";
import type { OptionsAnalytics } from "../frame/options-analytics.ts";
import { canonicalizeConfig, cellConfigId as cellConfigIdOf, deriveConfigId, envelopeForConfig, fillSeedOf, sampledQualificationOf } from "./config.ts";
import { carryFrom, emptyCarry, type SessionCarry } from "./carry.ts";
// The stowaway concepts extracted from this driver into sibling modules (kestrel-z473.1) — this file
// keeps ONLY the wake loop. Each is imported back for the loop's own use; the ones this file used to
// export are re-exported below so the public surface is byte-identical.
import { vehicleHealthOf, vehicleBookFromSpot, type VehicleBook } from "./vehicle-health.ts";
import { sizingHeadroomOf, budgetEnvelopeOf } from "./sizing.ts";
import {
marketPaneOf,
buildFrameOptions,
SERVED_TAPE_BUCKET_MIN,
type ChainFairContext,
type OptionsOverlayConfig,
} from "./market-pane.ts";
import {
runOpenAuthoringLoop,
viewSelectionOfDocument,
DEFAULT_VIEW_REQUEST_CAP,
DEFAULT_AUTHORING_TOKEN_BUDGET,
type EmergenceRecord,
} from "./authoring-loop.ts";
import { wakeFloorForBand, DAY_STALENESS_BACKSTOP_MIN, type FrontierWake, type WakeCtx } from "./wake-frontier.ts";
// Re-export the moved public symbols so `import { … } from ".../session/simulate.ts"` (and the
// `src/session/index.ts` barrel) resolve exactly as before — the extraction is API-transparent.
export { marketPaneOf, SERVED_TAPE_BUCKET_MIN, type ChainFairContext, type OptionsOverlayConfig } from "./market-pane.ts";
export { vehicleHealthOf, vehicleBookFromSpot, type VehicleBook } from "./vehicle-health.ts";
export { sizingHeadroomOf } from "./sizing.ts";
export {
viewSelectionOfDocument,
DEFAULT_VIEW_REQUEST_CAP,
DEFAULT_AUTHORING_TOKEN_BUDGET,
type EmergenceRecord,
} from "./authoring-loop.ts";
export { type WakeCtx } from "./wake-frontier.ts";
// ─────────────────────────────────────────────────────────────────────────────
// Public surface
// ─────────────────────────────────────────────────────────────────────────────
/**
* The DAY-COMPAT side-channel (kestrel-5zl.3) — the narrow bridge the {@link import("./day.ts").runDaySession}
* fileHandshakeAgent and the driver share so the day agent can render its inspectable artifacts + validate
* revisions against the LIVE core WITHOUT a forked progression path. It carries NOTHING onto the graded
* stream (it sits entirely ABOVE the determinism line): the driver sets `vantage` at each vantage BEFORE
* calling `agent.open`/`agent.decide`, and binds `previewSupersede` once to the live engine's pure collision
* preview. Present ONLY on the actual day file agent; a plain {@link dayCompat} run (e.g. a recordedAgent
* replay) passes no bridge and the driver never touches it.
*/
export interface DayCompatBridge {
/** The raw date-blind {@link AuthorFrame} for THIS vantage (OPEN briefing / each wake), set by the driver
* before the corresponding `agent.open`/`agent.decide`. The day agent names each wake artifact from it
* (`frame-<n>-<HHMM>.txt`) and serializes it as the `state-<n>.json` machine channel; the TEXT frames
* render through the canonical `src/frame` renderer over the driver's typed frames (kestrel-wa0j.2). */
vantage?: AuthorFrame;
/** The engine's PURE supersede-then-arm collision preview ({@link import("../engine/index.ts").PlanEngine}
* `supersedeArmRejection`), bound once by the driver to the live core: a rejection reason or `null`. The
* day agent's reject-retry consults it so the standing book is NEVER torn down for a doomed revision
* (fail-closed blast-radius, RUNTIME §8) — the collision preview guarantees the driver's subsequent
* supersede+arm cannot reject. */
previewSupersede?: (mod: Module) => string | null;
}
/** Options for {@link runSimulateSession}. Exactly one of `busPath`/`events` supplies the bus; the
* {@link Agent} seam supplies the decisions. The fill/tau/budget knobs mirror {@link import("./sim.ts").RunSimOptions}. */
export interface RunSimulateOptions {
readonly busPath?: string;
readonly events?: readonly BusEvent[];
/** The agent under test — live, recorded, fixed-plan, or file-handshake (the ONE seam). */
readonly agent: Agent;
/** Authored wake vantages (ET times-of-day), strictly inside the driven window. */
readonly wakes?: readonly TimeOfDay[];
/** The author acting cutoff (an ET wall-clock time-of-day). The OPEN briefing is built ONLY from
* observations watermarked at or before this instant; defaults to the canonical benchmark's T-5
* (09:25 ET). Injected + explicit so no post-cutoff print leaks to the agent (kestrel-m9i.11). */
readonly authorCutoff?: TimeOfDay;
/** Present the OPEN authoring vantage as an INTRADAY keyframe carrying the observed range
* (kestrel-1vw): when `true`, the OPEN briefing folds every exec observation at/before {@link
* authorCutoff} into the range (`hod`/`lod`/`tape` + the current spot) instead of the pre-open
* keyframe — so an ORB cell authoring at its opening-range close sees the range it must break out of.
* The causal cutoff fence is unchanged (no post-cutoff print leaks); this ONLY changes whether the
* admitted prefix is collapsed to the opening keyframe or presented as the range. Absent / `false` ⇒
* today's pre-open briefing, byte-identical. Set together with an intraday {@link authorCutoff}. */
readonly authorFromRange?: boolean;
/** Add the structural cadence into the close (T-2h…T-5m) — see {@link computeWakes}. */
readonly structural?: boolean;
readonly fillModel: FillModelName;
readonly rUsd: number;
/** The cell-sourced hard {@link Mandate} (ADR-0026) — the objective + R-definition + success
* criterion + bounded-risk rule the agent is graded against. Rendered in the kernel; the ONLY
* admission input. A GRADED run must declare one ({@link import("../frame/types.ts").assertGradedMandate});
* an ungraded/dev run may omit it (the frame renders unchanged). NOT hardcoded — the cell owns it. */
readonly mandate?: Mandate;
/** The cell-sourced soft {@link Brief} (ADR-0026) — directional English (goal/approach/persona),
* rendered as standing context, hashed + attributed, but NEVER an admission input (the hard guard). */
readonly brief?: Brief;
readonly instruments?: readonly InstrumentSpec[];
readonly fairTauYears?: FairTauProvider;
readonly makerFairParams?: EpisodeSigmoidParams;
/** The prior session's {@link SessionCarry} (the multi-session horizon design, kestrel-5zl.16). Its `positions` seed this
* session's opening book and its `document` is re-armed fresh (new plan-instance ids) BEFORE the OPEN
* turn. Absent — or an `emptyCarry()` — is today's flat open, byte-identical. */
readonly openingCarry?: SessionCarry;
/** `true` for a NON-final session of a multi-session Instance: settle carry-marks held inventory (it
* survives into the returned {@link SimulateResult.carry}) instead of forcing it to intrinsic. Absent /
* `false` — a standalone or the final session — is today's terminal intrinsic settle exactly. */
readonly carryClose?: boolean;
/** This session's ordinal `k` in its Instance (the multi-session horizon design, kestrel-5zl.16.4) — the relative-day
* coordinate `d{k}`. Absent for a standalone run ⇒ today's coordinate (no `d{k}` prefix), byte-identical. */
readonly sessionOrdinal?: number;
/** The bounded OPEN authoring loop's dual budget (ADR-0029 §2). Consulted ONLY when the agent
* exposes {@link Agent.authoringOpen} (the viewshop arm); a baseline/recorded/fixed run ignores it
* entirely (single-shot open, byte-identical). Both fields are config axes — a different budget is a
* different grid column. Absent ⇒ the conservative defaults ({@link DEFAULT_VIEW_REQUEST_CAP} /
* {@link DEFAULT_AUTHORING_TOKEN_BUDGET}). */
readonly authoring?: {
/** Max non-terminal iterations (view-requests + repairs, shared) before fail-closed standDown. */
readonly viewRequestCap?: number;
/** Cumulative model-token ceiling (input + output + thinking, summed across iterations). */
readonly authoringTokenBudget?: number;
};
/** The Percept v5 EMERGENCE LOG sink (ADR-0029 §5) — OFF the graded path. When supplied, the driver
* appends one {@link EmergenceRecord} per non-terminal authoring iteration (BOTH view-requests AND
* parse-error/repair events), keyed by `(OPEN_ORDINAL, iteration)`. Private application data (the raw
* data for the emergence study AND the grammar-evolution signal, ADR-0030); never a graded input. */
readonly emergenceLog?: EmergenceRecord[];
/**
* DAY-COMPAT MODE (kestrel-5zl.3 re-run) — the CALLER-OWNS-CADENCE path the {@link import("./day.ts").runDaySession}
* fileHandshakeAgent refactor drives through. When the caller owns the wake cadence (the day's structural
* offsets), the driver reproduces the STEPPED runner's cadence + byte stream EXACTLY:
* - NO injected platform-default staleness/backstop wakes for this path — the day's own structural wakes
* provide the coverage, so the delivered wake set is exactly `computeWakes(...)` (no 30-min DAY backstop
* churn), reproducing the frozen day determinism_hash 44cf2cbf;
* - CONTROL supersede carries the day's `{target:"wake-N", note:"revision-N"}` byte shape (not the bare
* `{note?}` the agent-driven path emits), so `serializeEvent` bytes match the stepped runner.
*
* This is NARROW: it is NOT "silence the backstop". It never weakens the band fail-closed logic (16.7) for a
* normal band run — {@link wakeFloorForBand}/{@link armStaleness} still floor every agent-driven session and
* {@link tests/simulate.wake-band.test.ts} stays green. Absent / `false` ⇒ today's full agent-driven frontier,
* byte-identical. (Threaded by the green step; declared here as the contract the TDD-red test targets.)
*/
readonly dayCompat?: boolean;
/** The DAY-COMPAT {@link DayCompatBridge} (kestrel-5zl.3) — present ONLY when the caller is the
* {@link import("./day.ts").runDaySession} file agent (it renders artifacts from `vantage` + validates
* revisions through `previewSupersede`). Off the graded line: the driver sets `vantage` before each
* `open`/`decide` and binds `previewSupersede` to the live core, but emits NOTHING from it. A plain
* `dayCompat` run (a recordedAgent replay) omits it and the driver never touches it. */
readonly dayBridge?: DayCompatBridge;
/** The options-analytics OVERLAY (kestrel-4gl.13.13) — the perception arm's OPRA surface, folded
* causally into `market.options` at every frame cutoff so the `gex`/`iv-skew`/`implied-realized`
* panes render in a LIVE run. **Config-gated: supplied ONLY for the perception arm** (a View that
* selects the options-analytics panes). Absent — the minimal arm — leaves `market.options`
* undefined and every frame byte-identical to before (the panes are off-default; the fold is never
* paid). Folded per-cutoff with the T-5m guard (only events at/before the cutoff, no look-ahead) and
* relabeled date-blind before it reaches the agent frame ({@link buildFrameOptions}). */
readonly optionsOverlay?: OptionsOverlayConfig;
/**
* THE MULTIDAY TAPE SEAM (kestrel-wa0j.20 — the DATA half of Train 1B's `tape d-N` address). The
* PRIOR sessions' tapes (date-blind: relative ordinals + HH:MM clocks, NEVER dates), threaded onto
* every frame this session serves so a View naming `tape d-N` (N ≥ 1) renders ordinal N's OWN
* single-session block instead of the Train 1B absence line. Each ordinal renders SEPARATELY — no
* cross-session folding (that is wa0j.20b, behind PR #194's render unification).
*
* INJECTABLE seam: the v1 single-day sim carries no multi-session tape state, so this is typically
* ABSENT (honestly — `tape d-N` then renders absent-with-reason, byte-identical). A multi-day harness
* (or a future day/backtest path that has the prior sessions' rows cheaply available) INJECTS them
* here; the driver threads them, unchanged, into both the OPEN briefing and every wake frame. Absent
* ⇒ no `priorSessions` field on any frame, byte-identical to today. */
readonly priorSessions?: readonly PriorSessionTape[];
/**
* THE THETA GATE (theta-cell seam b, {@link import("../fill/held-position.ts").heldPositionAsOfGradedBus}).
* When `true`, after settle the driver asks the held-position owner for the position held as of the
* graded bus — it reconstructs the net HELD option legs + their blended basis, re-marks them against the
* tape's own option book at every DELIVERED wake (theta decay between wakes), and returns ONE graded
* `TELEMETRY theta_bleed` record carrying the stand-down floor (the mark at the LAST wake), which the
* driver emits verbatim. The Blotter folds that into `totals.floor` (ADR-0011), so a watcher that STANDS PAT on a
* bleeding straddle realizes a graded NEGATIVE floor instead of paying no running cost for doing nothing
* — the whole point of the theta cell (it dissolves the doing-nothing-wins degenerate reward).
*
* Threaded from the FOMC-OPTIONS {@link import("./harness/plan-fixture.ts").FanFrame} `markToModel`.
* Absent / `false` (every other cell) ⇒ no reconstruction, no record, no fold — every existing pinned
* floor is byte-identical. Only binds when a position is actually HELD across ≥1 wake (a flat book, or a
* watcher that closed out, emits nothing). CLEAN BOUNDARY: the bleed rides the graded Bus / Blotter as a
* mark-to-model GRADING penalty; it is deliberately NOT folded into the engine-native SessionReport
* `realized_floor_usd` or the multi-session carry cash (that report/ledger integration is separate work).
*/
readonly markToModel?: boolean;
/**
* The injectable wall clock the driver measures a clock-honest Seat turn with (ADR-0040 /
* clock-honest wakes §7 — the ONE minting seam, the `opts.now ?? Date.now` idiom of `liveAgent`).
* Consulted ONLY in a LIVE-shaped clock-honest run: a latency-blind session never measures (its
* captures stay costless-deterministic), and a REPLAY (an agent exposing `recordedCostMs`) reads
* recorded costs and never touches this — a replay path that reads a clock is a defect by
* definition (the poisoned-clock fixture). Defaults to `Date.now`.
*/
readonly now?: () => number;
/**
* The session's DECLARED data rung (ADR-0040 data floor / clock-honest wakes §4) — the tape's
* observability provenance (`"tick" | "second" | "minute" | "session"`, the `GradingFidelity`
* lattice), declared on the run options / session catalog and carried with the tape like
* `fill_model`. Typed loosely (injected provenance bytes) and VERIFIED fail-closed via the isRung
* discipline: `tick`/`second` ⇒ clock-honest eligible; `minute`/`session` ⇒ latency-blind; an
* off-lattice or ABSENT rung ⇒ latency-blind (an untrusted rung never compares as eligible).
* Requesting `clockHonest: true` over an ineligible/undeclared rung REFUSES the run loudly at open —
* never a silent downgrade to latency-blind. Irrelevant to a latency-blind run (may be omitted).
*/
readonly dataRung?: string;
}
/** The result of a Simulate session: the graded {@link Blotter} (a PURE projection of the graded Bus,
* ADR-0011), the {@link CapturedTurns} the run consumed (the deterministic replay input for
* {@link recordedAgent}), and the agent's {@link AgentConfig} (the comparison axis). */
export interface SimulateResult {
readonly blotter: Blotter;
/**
* The native graded {@link SessionReport} of this run (kestrel-m9i.7.4) — the SAME `core.buildReport`
* projection {@link import("./day.ts").runDaySession} returns, computed here from the finalized core so a
* live/handshake Simulate run lands in the run {@link import("../ledger").ledger} WITHOUT a Blotter→report
* bridge (the tournament runner records this verbatim; `session.settle_ts` is the injected `recorded_at`).
* A PURE read of the finalized session — no wall clock, no RNG, and byte-identical to a `runDaySession`
* report of the same graded bus (RUNTIME §0).
*/
readonly report: SessionReport;
readonly captured: CapturedTurns;
readonly config: AgentConfig;
/** The canonical GRADED bus of this run (`core.gradedBus()` — the judge-stamped META + every emitted
* engine reaction, re-stamped gap-free): the record itself, surfaced so a caller (and the w7la.1
* clocked fixtures) can assert bus-level facts — deliberation records, checkpoints, refusal
* stamping — without any engine-state scrape (ADR-0010: the Bus is the truth). A PURE read of the
* finalized core; `project(gradedBus)` is byte-identical to {@link SimulateResult.blotter}. */
readonly gradedBus: readonly BusEvent[];
/** The {@link SessionCarry} this session hands to the next (the multi-session horizon design, kestrel-5zl.16) — a PURE read of
* the finalised session. On a final / standalone session (`carryClose` absent) its `positions` are
* empty (inventory settled at intrinsic). The runner ({@link import("./instance.ts").runInstance})
* threads it into the next session's `openingCarry`. */
readonly carry: SessionCarry;
}
// ─────────────────────────────────────────────────────────────────────────────
// Small helpers (pure, deterministic — no wall clock, no RNG)
// ─────────────────────────────────────────────────────────────────────────────
/** Normalize a parsed node into a module (a bare statement → a one-statement module) so it can be armed. */
function toModule(node: KestrelNode): Module {
return node.kind === "module" ? node : { kind: "module", imports: [], statements: [node] };
}
function msgOf(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
// ─────────────────────────────────────────────────────────────────────────────
// Clock-honest eligibility (ADR-0040 data floor / clock-honest wakes §4) — declared, verified, fail-closed
// ─────────────────────────────────────────────────────────────────────────────
/** The rungs of the observability lattice fine enough to ground a clock-honest session (ADR-0040:
* sub-minute ground truth — 1s bars / event streams). The isRung discipline (`src/series/registry.ts`)
* applied to THIS question: anything outside the closed lattice is untrusted and never compares as
* eligible. */
const CLOCK_HONEST_ELIGIBLE_RUNGS: ReadonlySet<string> = new Set(["tick", "second"]);
const OBSERVABILITY_RUNGS: ReadonlySet<string> = new Set(["tick", "second", "minute", "session"]);
/**
* Why a declared data rung CANNOT ground a clock-honest session, or `null` when it is eligible
* (clock-honest wakes §4). PURE and fail-closed — declared-then-verified is not trust:
* - `"tick"` / `"second"` → eligible (`null`);
* - `"minute"` / `"session"` → too coarse (the ADR-0040 data floor) — such a session stays VALID but
* latency-blind, and may never claim clock-honesty;
* - absent → undeclared provenance — refuse (never a silent default);
* - off-lattice → an untrusted rung — refuse (never compared as coarser/finer than anything).
* The caller (the driver, at open) REFUSES a `clockHonest: true` run whose rung returns non-null —
* never a silent downgrade to latency-blind (a producer must never *believe* it bought a
* latency-honest result it didn't get).
*/
export function clockHonestIneligibility(declaredRung: string | undefined): string | null {
if (declaredRung === undefined) {
return "the session declares NO data rung (`dataRung`) — clock-honest requires sub-minute ground truth declared with the tape (ADR-0040 data floor); refused at open, never a silent default";
}
if (!OBSERVABILITY_RUNGS.has(declaredRung)) {
return `declared data rung ${JSON.stringify(declaredRung)} is not on the observability lattice (tick, second, minute, session) — an untrusted rung never compares as clock-honest eligible (fail-closed)`;
}
if (!CLOCK_HONEST_ELIGIBLE_RUNGS.has(declaredRung)) {
return `declared data rung "${declaredRung}" is too coarse for clock-honest semantics (ADR-0040 data floor: tick/second only) — run it latency-blind or declare finer ground truth; refused at open, never a silent downgrade`;
}
return null;
}
// ─────────────────────────────────────────────────────────────────────────────
// Schedule-time View validation (kestrel-wa0j.19 §1b) — the driver-side guard the wake loop applies
// when a `scheduleWake` is accepted. (The bounded OPEN authoring loop itself + the View-document →
// ViewSelection mapper live in `./authoring-loop.ts`; the wake floors + frontier shapes in
// `./wake-frontier.ts`; extracted from this driver in kestrel-z473.1.)
// ─────────────────────────────────────────────────────────────────────────────
/**
* SCHEDULE-TIME View value/budget validation (kestrel-wa0j.19 §1b): the checks that are KNOWABLE when a
* `scheduleWake` is accepted — BEFORE the View ever reaches an (otherwise unguarded) delivery render.
* Returns a refusal reason when the resolved View carries a defect the constant session context can prove
* NOW, else `null`. Two checks, both mirroring the exact guards `materializePanes` applies at delivery so
* the two can never drift:
* • **window-arg value** — the served tape bucket is a session constant ({@link SERVED_TAPE_BUCKET_MIN}),
* so a `tape <window>` that is sub-resolution or a non-multiple of it is refusable now via the SHARED
* {@link windowServableBy} predicate (the same one the arged tape builder enforces at materialization).
* • **budget sanity** — if the View declares a token budget already below the SUM of its selected panes'
* catalog `tokenCostEstimate`s, it can never fit; refuse now. (The exact per-tokenizer measurement at
* materialization stays the precise guard — the delivery seam §1a is the fail-closed backstop for a
* View that fits the estimate yet overruns the real count.)
* PURE. Distinct from the STRUCTURAL/date-leak refusals (unknown pane / reserved id / calendar leak), which
* stay whole-wake refusals: a structurally-broken or unsafe View is nonsense to honor, whereas a sound View
* that this session's resolution simply cannot serve drops to the default panes while the wake still fires.
*/
export function scheduleTimeViewDefect(sel: ViewSelection): string | null {
for (const p of sel.panes) {
for (const a of p.args ?? []) {
if (a.kind === "arg-window" && p.name === "tape" && !windowServableBy(SERVED_TAPE_BUCKET_MIN, a.window)) {
return (
`pane "tape" window "${a.window.value}${a.window.unit}" is not servable — the tape rows are served ` +
`at ${SERVED_TAPE_BUCKET_MIN}m, so a renderable window must be a positive multiple of ${SERVED_TAPE_BUCKET_MIN}m`
);
}
}
}
if (sel.budget !== undefined) {
let est = 0;
for (const p of sel.panes) {
const entry = paneById(p.name);
if (entry !== undefined) est += entry.tokenCostEstimate;
}
if (est > sel.budget) {
return `View "${sel.name}" is over its own token budget: the selected panes' estimated cost (~${est} tokens) exceeds the declared budget of ${sel.budget}`;
}
}
return null;
}
/** `HH:MM` ET clock for a ts (dateless — a clock pins no calendar date). */
function clockOf(ts: number): string {
const m = ((etMinuteOfDay(ts) % 1440) + 1440) % 1440;
return `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
}
const FRAME_PHASES = new Set<string>(["pre", "open", "regular", "close", "post"]);
/** Map the snapshot's phase string to a {@link FramePhase}, or `null` when unresolved (never a guess). */
function mapPhase(phase: string): FramePhase | null {
return FRAME_PHASES.has(phase) ? (phase as FramePhase) : null;
}
// ─────────────────────────────────────────────────────────────────────────────
// AuthorFrame → frame/types mappers (the acting Frame is assembled from the SAME date-blind projection
// the stepped day emits — relative time + relative dte only, so no channel can re-leak the calendar)
// ─────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Options-analytics overlay fold (kestrel-4gl.13.13) — the perception arm's live `market.options`
//
// The driver (the impure orchestrator) folds the OPRA overlay tape causally to each frame cutoff and
// hands the pure frame builders the finished, date-blind {@link OptionsAnalytics}. Kept OUT of the
// pure `projectWakeFrame`/`briefingInputOf` seams (they receive the built projection, never the tape),
// so those stay pure functions of their inputs.
// ─────────────────────────────────────────────────────────────────────────────
/** Stream `events` up to and including `tsMax` — the causal T-5m cutoff (no look-ahead). A lazy
* generator over a lazy source reads only the prefix (the fold stops at the first later event). */
function* untilTs(events: Iterable<BusEvent>, tsMax: number): Generator<BusEvent> {
for (const e of events) {
if (e.ts > tsMax) return;
yield e;
}
}
/** The plan-lifecycle HALF of the {@link Kernel} superset (positions / resting / fills / budget /
* plan states), scraped from the date-blind snapshot. The WAKE frame layers the enriched 4gl.3
* cockpit lead block on top of this base ({@link enrichedKernelOf}); the OPEN briefing carries the
* base alone (the enriched cockpit at OPEN is out of scope for this slice — m9i.11 briefing). */
/** The cell-sourced STANDING context (ADR-0026) layered onto every frame's kernel: the hard
* {@link Mandate} (the only admission input) + the soft, directional {@link Brief}. Sourced from
* the SESSION/CELL ({@link RunSimulateOptions}), NOT hardcoded — different cells declare different
* objectives. Frame content sits ABOVE the determinism line, so this never perturbs the graded bus. */
export interface KernelStanding {
readonly mandate?: Mandate;
readonly brief?: Brief;
}
/** Roll same-(instrument, strike, right) fill legs into ONE net position line (kestrel-dd8): the fill
* engine SETTLES these netted, so the pane must too — a GROSS per-leg render shows huge OFFSETTING marks
* that misread as a phantom winner (`+$18,000` on a net-zero book) or invite a churn to "flatten" an
* already-flat book. Net qty is the signed sum (a net-0 pair renders a single clearly-flat `0 …` line —
* already flat, nothing to churn); the blended basis is the ABS-qty-weighted average of the leg bases
* (always well-defined, even for a net-zero pair); the net `unrealUsd` is the SUM of the per-leg unreal
* (which nets to the TRUE figure). If ANY grouped leg's mark is UNKNOWN (`null`) the net is `null`
* (fail-closed — never sum a fabricated 0 into a real figure); if EVERY leg omits it the net omits it.
* Grouping preserves each group's FIRST-APPEARANCE order (deterministic given the deterministic fills),
* so a book with NO same-(strike,right) duplicates is BYTE-IDENTICAL to the un-netted projection. Pure. */
function netPositionLegs(legs: readonly Position[]): Position[] {
const groups = new Map<string, Position[]>();
for (const p of legs) {
const key = `${p.instrument} ${p.strike} ${p.right}`;
const grp = groups.get(key);
if (grp === undefined) groups.set(key, [p]);
else grp.push(p);
}
const out: Position[] = [];
for (const grp of groups.values()) {
if (grp.length === 1) {
out.push(grp[0]!); // the common case — passed through untouched (byte-identical)
continue;
}
const first = grp[0]!;
const qty = grp.reduce((n, p) => n + p.qty, 0);
const absQty = grp.reduce((n, p) => n + Math.abs(p.qty), 0);
const basis = absQty > 0 ? grp.reduce((s, p) => s + Math.abs(p.qty) * p.basis, 0) / absQty : first.basis;
const anyUnknown = grp.some((p) => p.unrealUsd === null);
const allAbsent = grp.every((p) => p.unrealUsd === undefined);
const unrealUsd: number | null | undefined = allAbsent ? undefined : anyUnknown ? null : grp.reduce((s, p) => s + (p.unrealUsd ?? 0), 0);
// Provenance survives only when every leg agrees; a mixed group has no single opener (drop it).
const plan = grp.every((p) => p.plan === first.plan) ? first.plan : undefined;
out.push({
instrument: first.instrument,
// ADR-0017: an equity/spot leg carries NEITHER strike NOR right — preserve their absence through the
// netting (a spot group nets under the `NVDA undefined undefined` key), so the frame renders it as
// `<instrument> shares`, never a phantom strike. An option group carries both (byte-identical).
...(first.strike !== undefined ? { strike: first.strike } : {}),
...(first.right !== undefined ? { right: first.right } : {}),
qty,
basis,
...(plan !== undefined ? { plan } : {}),
...(unrealUsd !== undefined ? { unrealUsd } : {}),
});
}
return out;
}
export function kernelOf(
af: AuthorFrame,
instrument: string,
standing: KernelStanding = {},
// kestrel-c11: the exec contract multiplier (equity/spot 1, option 100) — the SAME scaling the fill
// engine's `pnl` applies (src/engine/orgfacts.ts), threaded so the surfaced P&L is in DOLLARS, never
// per-contract points. Defaults to 1 (equity/spot); the OPEN briefing carries no fills so it is moot.
multiplier: number = 1,
): Kernel {
const spot = af.spot;
const markKnown = spot !== null && Number.isFinite(spot);
// Per-fill legs (kestrel-c11 running $ P&L each), then NETTED per (instrument, strike, right) so the
// pane matches the engine's netted settle (kestrel-dd8) — a gross render misreads offsetting marks.
const legs: Position[] = af.fills.map((f) => {
const qty = f.side === "buy" ? f.qty : -f.qty;
// kestrel-c11 — the position's running unrealized P&L in DOLLARS: `qty × (mark − basis) × multiplier`,
// marked to spot (equity/spot, ADR-0017) or intrinsic (option), with the SAME dollar scaling the fill
// engine's `pnl` uses. So the agent READS `unreal=-$25.97` instead of deriving it and mis-scaling
// cents-for-dollars (the 100× abandon bug). `null` when the mark is UNKNOWN (no spot) — fail-closed to
// `—`, never a fabricated 0. An option leg carries a finite strike + a real C/P right; equity does not.
const isOption = Number.isFinite(f.strike) && (f.right === "C" || f.right === "P");
const mark = !markKnown ? null : isOption ? intrinsic(spot as number, f.strike, f.right as Right) : (spot as number);
// single exec instrument per session (ADR-0017); a mixed-multiplier book would need a per-instrument multiplierOf like orgfacts.
const unrealUsd = mark === null ? null : qty * (mark - f.px) * multiplier;
return {
instrument,
strike: f.strike,
right: f.right as FrameRight,
qty,
basis: f.px,
plan: f.plan,
unrealUsd,
};
});
// kestrel-dd8: net same-(instrument, strike, right) legs into ONE line so the pane matches the engine's
// netted settle — a book with no duplicates is byte-identical to the un-netted per-fill projection.
const positions: Position[] = netPositionLegs(legs);
const resting: RestingOrder[] = af.resting.map((r) => ({
// The AUTHORITATIVE Gate ref the fill engine rests this order under (kestrel-5zl.5): a `cancelOrder`
// carrying this ref resolves to the real resting order through the SAME Gate a fired Plan cancels
// through. (It was a synthetic `plan:role:strikeRight` handle that no Gate ref ever matched, so
// cancel-by-ref silently no-op'd.) The kernel PRINTS it (`ref=o1`) — the ref is what a cancel names.
ref: r.ref,
side: r.side as "buy" | "sell",
instrument,
strike: r.strike,
right: r.right as FrameRight,
qty: r.qty,
px: r.px,
plan: r.plan,
}));
const plans: PlanStateEntry[] = af.plans.map((p) => ({
name: p.name,
state: p.state as PlanStateEntry["state"],
// kestrel-50w: carry the arm-time gate-block reason into the acting Frame so a plan stuck `authored`
// on an unsatisfiable regime gate renders `authored (blocked: <reason>)`, never a bare (live-looking)
// `authored`. Frame content, above the determinism line — the graded bus is untouched.
...(p.blockedReason !== undefined ? { blockedReason: p.blockedReason } : {}),
}));
return {
// Cell-sourced standing context (ADR-0026) — present only when the cell declared it (absent ⇒
// the frame renders unchanged, byte-identical to pre-mandate). Never a fabricated default.
...(standing.mandate !== undefined ? { mandate: standing.mandate } : {}),
...(standing.brief !== undefined ? { brief: standing.brief } : {}),
positions,
resting,
// Per-vantage fill deltas are a later refinement; the authoritative fill record is the graded Bus.
fillsSinceLast: [] as FillRecord[],
budget: { used: af.committedUsd, remaining: af.rUsd - af.committedUsd, total: af.rUsd },
plans,
};
}
/** The OPEN briefing input, mapped from the date-blind briefing {@link AuthorFrame}. */
function briefingInputOf(
af: AuthorFrame,
meta: MetaEvent,
instrument: string,
firstTs: number,
lastTs: number,
standing: KernelStanding = {},
options?: OptionsAnalytics,
// kestrel-121: the chain-fair context evaluated at the OPEN author cutoff. Absent ⇒ the briefing
// chain `fair` stays `—` (byte-identical to a pre-fair briefing).
fairCtx?: ChainFairContext,
// kestrel-wa0j.20: the injected prior-session tapes, threaded onto the OPEN briefing so `tape d-N`
// serves ordinal N's block. Absent ⇒ no `priorSessions` field (byte-identical, Train 1B absence).
priorSessions?: readonly PriorSessionTape[],
): BriefingInput {
const instruments: FrameInstrumentSpec[] = meta.instruments.map((si) => ({
symbol: si.symbol,
assetClass: si.assetClass,
...(si.role !== undefined ? { role: si.role } : {}),
}));
// The PRESENTED current-time is the frame's OWN vantage, reconstructed from its relative `sinceOpenMs`
// (projected against this same `firstTs` origin) — NOT a re-assumption that the author acts at the
// open. For the default (pre-open) briefing `sinceOpenMs === 0`, so `vantageTs === firstTs` and both
// fields are byte-identical to before. For an intraday `authorFromRange` briefing the vantage is the
// acting cutoff (kestrel far-cutoff clock), so the clock reads the cutoff and time-to-close shrinks
// accordingly — the honest "it's 13:45, ~2h15m to close" a live FOMC author must reason over.
const vantageTs = firstTs + af.sinceOpenMs;
return {
timeToCloseMin: Math.round((lastTs - vantageTs) / 60_000),
phase: mapPhase(af.phase),
clockET: clockOf(vantageTs),
instruments,
market: marketPaneOf(af, instrument, options, fairCtx),
kernel: kernelOf(af, instrument, standing),
...(priorSessions !== undefined ? { priorSessions } : {}),
};
}
// ─────────────────────────────────────────────────────────────────────────────
// The agent-driven wake frontier (kestrel-5zl.4) — constants + shape
//
// After the T-5 OPEN baseline (m9i.11) the AGENT drives its own monitoring cadence via `scheduleWake`;
// the platform FLOORS it (never replaces it) with a staleness backstop + the optional structural close
// cadence, and coalesces/downgrades past the attention budget. These are author-tunable config knobs,
// NOT removable by the agent — safety can never be silenced (CLAUDE fail-closed). The budget algebra
// proper and per-session structural re-anchoring are scoped to the b3l Wake-router epic.
// ─────────────────────────────────────────────────────────────────────────────
/** Total, deterministic frontier tiebreak when two wakes share a ts: the agent first, then the author
* seed, then an own-fill management wake, then the structural cadence, then the staleness backstop (safety
* sorts last, never masking an earlier authored intent). Same bus + same agent turns ⇒ identical frontier
* order ⇒ identical Blotter. The `own-fill` wake exists only on a seeded (sampled-channel) run — a
* fire-then-inform management prompt raised when the agent's order fills, ADR-0016 / kestrel-9gu.8.3.
* `latency-fold` (ADR-0040 / clock-honest wakes §2.3) is a determinism-bearing tie-break PIN: no tie is
* currently reachable — a frontier wake at exactly `returnTs` folds INTO the catch-up rather than sorting
* beside it — but the rank is pinned anyway so the sort stays total (RUNTIME §0). */
const WAKE_SOURCE_RANK: Record<WakeSource, number> = { agent: 0, seed: 1, "own-fill": 2, structural: 3, staleness: 4, "latency-fold": 5 };
// ─────────────────────────────────────────────────────────────────────────────
// The SeqWindow + engine-actions-since-last-wake (kestrel-5zl.11 — the 4gl.3 cockpit engine log)
//
// The frame's L0/L1 engine log is the ACTIONS-SINCE-THE-LAST-WAKE, not the whole session: the
// `[prevWakeSeq, thisWakeSeq)` slice of the graded Bus in **seq space** (every event carries a
// monotonic `seq`, the determinism key — bus/types.ts EnvelopeBase). `prevWakeSeq`/`thisWakeSeq`
// are the two adjacent WAKE-checkpoint seqs the driver stamps at each vantage. Reading only events
// with `seq < thisWakeSeq` is frozen-at-wake by construction (no look-ahead), and an `asofSeq` is an
// ORDINAL (date-blind), so the window is deterministic and replay-stable.
// ─────────────────────────────────────────────────────────────────────────────
/** The `[prevWakeSeq, thisWakeSeq)` bus-seq window the engine-actions-since-last-wake ride. Both are
* WAKE-checkpoint `seq`s (an ordinal, never a wall-clock/date); the half-open window is the gap
* between the previous vantage and this one. */
export interface SeqWindow {
readonly prevWakeSeq: number;
readonly thisWakeSeq: number;
}
/** The pure inputs {@link projectWakeFrame} consumes beyond the graded bus + the seq window: the
* date-blind vantage projection (market pane + plan-lifecycle base), the exec instrument, the
* per-vehicle books for the cockpit data-health, and the wake ctx (ordinal / reason / deadlines /
* attention). Everything here is a pure derivation of the injected bus (no wall clock, no RNG). */
export interface WakeFrameConfig {
readonly af: AuthorFrame;
readonly instrument: string;
/** The exec instrument's contract multiplier (kestrel-m9i.32) — `1` for equity/spot, `100` for a
* listed option. Drives the cost-basis sizing headroom (equity basis = spot × mult; option basis =
* premium × mult, ADR-0017). Absent ⇒ treated as `1` (equity/spot). */
readonly execMultiplier?: number;
readonly vehicles: readonly VehicleBook[];
readonly ctx: WakeCtx;
/** The cell-sourced standing context (ADR-0026 mandate + brief) layered onto the wake kernel. */
readonly standing?: KernelStanding;
/** The options-analytics projection for THIS wake's cutoff (kestrel-4gl.13.13) — already folded
* causally + date-blind by the driver ({@link buildFrameOptions}). Present only for the perception
* arm; absent leaves `market.options` undefined (the minimal arm, byte-identical). */
readonly options?: OptionsAnalytics;
/** The injected time-to-expiry resolver (`fairTauYears`) the chain's `fair` column is priced through
* (kestrel-121) — the SAME source the fill engine prices `@fair` with, evaluated at THIS wake's cutoff
* (`ctx.wakeTs`). Absent ⇒ the chain `fair` stays `—` (byte-identical to a pre-fair frame). */
readonly fairTauYears?: FairTauProvider;
/** kestrel-wcnd: the exec chain's OWN expiry token at this wake (from the RAW snapshot — the
* date-blind `af` cannot carry it). Feeds `fairTauYears`'s 2nd argument ONLY; never rendered. */
readonly fairExpiry?: string;
/** The prior author vantage's SERVED values (kestrel-wa0j.48) — the typed snapshot the driver served
* at the PREVIOUS vantage, so the `delta` pane can state what MOVED under stateless-redraw. Present
* only on a wake AFTER the first (the driver captures it from the prior frame's served levels/book);
* absent ⇒ the frame carries no `priorVantage` (byte-identical, and the delta pane fails closed). */
readonly priorVantage?: PriorVantage;
/** The PRIOR sessions' tapes (kestrel-wa0j.20) — the injected multiday tape (`opts.priorSessions`),
* threaded so `tape d-N` serves ordinal N's rows as its own block. Absent in the v1 single-day sim ⇒
* the frame carries no `priorSessions` (byte-identical, and `tape d-N` renders the Train 1B absence). */
readonly priorSessions?: readonly PriorSessionTape[];
/** Every armed plan STATEMENT the driver holds, in arm order (kestrel-wa0j.29) — the typed ASTs the
* `armed-plan` pane's terms are captured from. The seam filters these to the plans in an ENFORCED
* lifecycle state (per this wake's kernel plan set) and prints each clause through the canonical
* printer. Absent (or no enforced plan) ⇒ the frame carries no `armedPlan` and the pane fails closed
* to absent-with-reason, byte-identical to a frame without the field. */
readonly armedPlanAsts?: readonly PlanStatement[];
/** The tape's `session_date` (kestrel-wa0j.74) — the reference an ABSOLUTE per-leg `exp 2026-08-21`
* is date-blinded to a relative `<n>dte` against when the `armed-plan` pane captures the plan's terms.
* Required so an authored iso-date tenor cannot leak into the acting Frame and trip {@link
* assertFrameDateBlind}; unused when no plan carries an absolute per-leg expiry (byte-identical). */
readonly sessionDate: string;
}
/** Map ONE graded-bus event to its {@link EngineAction} bucket, or `null` when it is not an L0/L1
* engine action (RUNTIME §5–6): an `ORDER place` FIRED, an `ORDER cancel` (reason `cancelled`)
* CANCELLED, an `ORDER reject` REJECTED, a `TELEMETRY guard` whose directional cap was `applied`
* CLAMPED. An `ORDER fill` and an esc-reprice cancel (a non-`cancelled` reason) are not engine
* actions on this SAFETY/CONTROL surface. `id` is the order ref; `asofSeq` is the event's ordinal. */
/** Refusal-reason markers (engine-authored constant strings) that promote a PLAN lifecycle record to a
* `rejected` engine action so the acting agent sees WHY an order was refused (kestrel-7kt). A normal
* lifecycle reason (`fired`, `superseded`, `ttl`, `invalidate`, `cancel-if`, `transferred`) is NOT a
* refusal and stays out of the engine log.
*
* The list must cover EVERY reason `PlanEngine.#emitReject` authors (src/engine/plans.ts — the sole
* producer of a PLAN refusal notice; a plain `#transition` reason is open-vocabulary lifecycle, which is
* why this seam matches rather than inverts). Matching is the only discriminator the bus schema affords:
* a reject notice and a transition are the same `PLAN lifecycle` shape, so an unlisted refusal is dropped
* SILENTLY at the agent's only feedback loop (kestrel-x4j8). Adding an `#emitReject` reason means adding
* its marker here — tests/simulate.engine-log-refusals.test.ts pins the vocabulary. */
const REFUSAL_MARKERS: readonly string[] = [
"never naked",
"uncovered sell",
"unfillable",
"unresolvable",
"exceeds plan budget",
// kestrel-x4j8 — the fire-time refusals the 7kt list missed, each silently invisible until now:
// the fail-closed far-OTM cap on unresolved moneyness (plans.ts, kestrel-9gu.6 — its call site asks
// for it to be surfaced LOUDLY; `unresolvable` above never matched the engine's `unresolved`), the
// budget-exhausted peg that can no longer chase, and the reprice dropped onto an already-filled intent.
"unresolved",
"fill-guard",
"peg capped",
"reprice dropped",
// kestrel-ocyf — the arm-time regime activation-gate notices (`armDocument`): the definitive
// dead-on-arrival verdict on a pre-scanned regime-less tape, and the conditional not-yet-written
// block when the future is unknown. Both are #emitReject-authored and must reach the wake frame's
// engine log, or the acting agent re-learns the phantom-position trap one wake late.
"never written",
"not yet written",
];
function isRefusalReason(reason: string | undefined): boolean {
return reason !== undefined && REFUSAL_MARKERS.some((m) => reason.includes(m));
}
function engineActionOf(e: BusEvent): EngineAction | null {
if (e.stream === "ORDER") {
switch (e.type) {
case "place":
return { id: e.order_id, kind: "fired", asofSeq: e.seq };
case "cancel":
return e.reason === "cancelled" ? { id: e.order_id, kind: "cancelled", asofSeq: e.seq } : null;
case "reject":
return { id: e.order_id, kind: "rejected", asofSeq: e.seq, ...(e.reason !== undefined ? { reason: e.reason } : {}) };
default:
return null; // fill — not an engine action bucket
}
}
if (e.stream === "TELEMETRY" && e.t