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.
735 lines • 128 kB
JavaScript
/**
* # 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 { readBus } from "../bus/index.js";
import { intrinsic } from "../fair/index.js";
import { assertNever, parse, print } from "../lang/index.js";
import { armedPlanTermsOf, ENFORCED_PLAN_STATES } from "./armed-plan-terms.js";
import { decisionControlFields, driverMeasurementVersions } from "../grade/index.js";
import { heldPositionAsOfGradedBus } from "../fill/index.js";
import { project } from "../blotter/index.js";
import { expiryTauYears, etMinuteOfDay, etWallClockMs } from "./clock.js";
import { SessionCore, busSha256Of, fidelityOf, requireMeta, resolveSpecs } from "./sim.js";
import { authorCutoffTs, briefingSnapshot, computeWakes, projectAuthorFrame, scanDateLeak, snapshotOf, } from "./day.js";
import { OPEN_WAKE_KEY, assertHandlerResponseTimeless, wakeKeyOf, } from "./agent.js";
import { resolveView, windowServableBy, paneById, atmStraddleExtrinsic } from "../frame/pane-catalog.js";
import { canonicalizeConfig, cellConfigId as cellConfigIdOf, deriveConfigId, envelopeForConfig, fillSeedOf, sampledQualificationOf } from "./config.js";
import { carryFrom, emptyCarry } from "./carry.js";
// 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 } from "./vehicle-health.js";
import { sizingHeadroomOf, budgetEnvelopeOf } from "./sizing.js";
import { marketPaneOf, buildFrameOptions, SERVED_TAPE_BUCKET_MIN, } from "./market-pane.js";
import { runOpenAuthoringLoop, viewSelectionOfDocument, DEFAULT_VIEW_REQUEST_CAP, DEFAULT_AUTHORING_TOKEN_BUDGET, } from "./authoring-loop.js";
import { wakeFloorForBand, DAY_STALENESS_BACKSTOP_MIN } from "./wake-frontier.js";
// 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 } from "./market-pane.js";
export { vehicleHealthOf, vehicleBookFromSpot } from "./vehicle-health.js";
export { sizingHeadroomOf } from "./sizing.js";
export { viewSelectionOfDocument, DEFAULT_VIEW_REQUEST_CAP, DEFAULT_AUTHORING_TOKEN_BUDGET, } from "./authoring-loop.js";
export {} from "./wake-frontier.js";
// ─────────────────────────────────────────────────────────────────────────────
// 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) {
return node.kind === "module" ? node : { kind: "module", imports: [], statements: [node] };
}
function msgOf(e) {
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 = new Set(["tick", "second"]);
const OBSERVABILITY_RUNGS = 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) {
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) {
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) {
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(["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) {
return FRAME_PHASES.has(phase) ? phase : 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, tsMax) {
for (const e of events) {
if (e.ts > tsMax)
return;
yield e;
}
}
/** 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) {
const groups = new Map();
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 = [];
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 = 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, instrument, standing = {},
// 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 = 1) {
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 = 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, f.strike, f.right) : spot;
// 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,
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 = netPositionLegs(legs);
const resting = 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,
instrument,
strike: r.strike,
right: r.right,
qty: r.qty,
px: r.px,
plan: r.plan,
}));
const plans = af.plans.map((p) => ({
name: p.name,
state: p.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: [],
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, meta, instrument, firstTs, lastTs, standing = {}, options,
// 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,
// 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) {
const instruments = 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 = { agent: 0, seed: 1, "own-fill": 2, structural: 3, staleness: 4, "latency-fold": 5 };
/** 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 = [
"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) {
return reason !== undefined && REFUSAL_MARKERS.some((m) => reason.includes(m));
}
function engineActionOf(e) {
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.type === "guard" && e.directional_cap === "applied") {
return { id: e.order_id, kind: "clamped", asofSeq: e.seq };
}
// A never-naked SELL refusal (or a synthesized-order de-arm) is a PLAN lifecycle record, not an ORDER
// reject — no order was ever submitted. Surface the REFUSAL ones (only) as a `rejected` engine action
// carrying the reason, so the agent can see why its order never rested (kestrel-7kt).
if (e.stream === "PLAN" && e.type === "lifecycle" && isRefusalReason(e.reason)) {
return { id: e.plan, kind: "rejected", asofSeq: e.seq, reason: e.reason };
}
// An agent order-action refuse (cancelOrder no-such-ref, scheduleWake unresolvable/past/beyond) is a
// CONTROL record — surface it too, so a refused agent action is never invisible to the agent.
if (e.stream === "CONTROL" && e.type === "refuse") {
const note = e.note ?? "refused";
return { id: note.split(":")[0].trim(), kind: "rejected", asofSeq: e.seq, reason: note };
}
return null;
}
/** The FILLS-SINCE-LAST-WAKE: the `[prevWakeSeq, thisWakeSeq)` slice of the graded stream's ORDER
* `fill` events, mapped to {@link FillRecord}. Frozen-at-wake (only `seq < thisWakeSeq`) and scoped to
* the gap (not cumulative-since-open), so an order that filled between vantages reaches the acting agent
* (kestrel-fud — this was hard-coded `[]`). Pure + deterministic, a projection of the graded bus. */
function fillsInWindow(emitted, window) {
const out = [];
for (const e of emitted) {
if (e.seq < window.prevWakeSeq || e.seq >= window.thisWakeSeq)
continue; // outside [prev, this)
if (e.stream !== "ORDER" || e.type !== "fill")
continue;
out.push({
side: e.side,
instrument: e.instrument,
// ADR-0017 (kestrel-1kp): an equity/spot fill carries NEITHER strike NOR right — PRESERVE their
// absence, never substitute a phantom `0`/`"C"` zero-strike CALL. A concrete `0`/`"C"` defeats
// {@link isSpotLeg} (which keys on presence), so the since-last fill line MISRENDERED the share leg
// as `0C` instead of `<instrument> shares` (the same phantom-chrome class kestrel-orx fixed on the
// kernel/positions surfaces). An OPTION fill carries BOTH — byte-identical to before.
...(e.strike !== undefined ? { strike: e.strike } : {}),
...(e.right !== undefined ? { right: e.right } : {}),
qty: e.qty,
px: e.px ?? 0,
clock: clockOf(e.ts),
...(e.plan !== undefined ? { plan: e.plan } : {}),
});
}
return out;
}
/** The engine-actions-since-the-last-wake: the `[prevWakeSeq, thisWakeSeq)` slice of the graded
* `emitted` stream, bucketed by {@link engineActionOf}. Frozen-at-wake (only events with
* `seq < thisWakeSeq`) and scoped to the gap (not cumulative-since-open). Pure + deterministic. */
function engineActionsInWindow(emitted, window) {
const out = [];
for (const e of emitted) {
if (e.seq < window.prevWakeSeq || e.seq >= window.thisWakeSeq)
continue; // outside [prev, this)
const a = engineActionOf(e);
if (a !== null)
out.push(a);
}
return out;
}
/** Layer the enriched 4gl.3 cockpit lead block onto the plan-lifecycle base {@link Kernel}: the wake
* ref (RELATIVE `T-Nm` deadline), per-vehicle data-health, the (empty) unavailable set, the cockpit
* budget envelope, owner acts, the engine-actions-since-last-wake, and predictor/regime claims. Every
* section is always present (absent renders as an explicit `none`/`UNKNOWN`, never dropped). This is
* the REAL kernel {@link renderKernel} consumes — not a partial stub. Pure + deterministic. */
function enrichedKernelOf(base, dataHealth, engineLog, ctx, sizing) {
const minutesToClose = Math.round((ctx.lastTs - ctx.wakeTs) / 60_000);
const wake = {
reason: ctx.wakeReason,
severity: "routine",
deadline: Number.isFinite(minutesToClose) ? minutesToClose : null,
};
return {
...base,
// The extended cockpit lead block (4gl.3) — always present (absent-not-hidden).
wake,
dataHealth,
// kestrel-wa0j.47: the v1 harness serves no macro/calendar capability, and the kernel's
// unavailable-capabilities line is now that absence's ONE rendering (the `macro` pane left the
// default OPEN set) — an empty list here would hide the absence entirely (absent-not-hidden).
unavailable: ["macro calendar"],
budgetEnvelope: budgetEnvelopeOf(base.budget, sizing),
ownerActs: [],
engineLog,
claims: [],
};
}
/**
* **The pure WAKE-frame builder** (kestrel-5zl.11): `(graded bus, seq window, config) → ActingFrame`.
* Assembles the date-blind {@link ActingFrame} handed to the agent at one wake, whose `kernel` is the
* REAL enriched 4gl.3 cockpit lead block — positions / resting / budget / data-health / owner-acts /
* the engine-actions-since-last-wake / predictor-regime claims — that {@link renderKernel} consumes,
* NOT the partial stub. The engine log is the `[prevWakeSeq, thisWakeSeq)` bus-window slice
* ({@link engineActionsInWindow}), so it is the since-LAST-wake delta, frozen-at-wake (only
* `seq < thisWakeSeq`) with no look-ahead.
*
* PURE: no wall clock, no unseeded RNG — the output is a byte-identical function of its inputs, so the
* same bus + same seq window + same config projects an identical frame (the determinism the tracer's
* replay rides on). Frame content sits ABOVE the determinism line (recorded/fixed adapters ignore it),
* so it never perturbs the graded bus. DATE-BLIND: only relative time, the `HHMM` ET clock, and
* ordinals reach the frame; {@link assertFrameDateBlind} is the fail-closed backstop at the seam.
*/
export function projectWakeFrame(emitted, window, config) {
const { af, instrument, vehicles, ctx, standing, options } = config;
const minutesToClose = Math.round((ctx.lastTs - ctx.wakeTs) / 60_000);
const dataHealth = vehicles.map((v) => vehicleHealthOf(v, ctx.wakeTs));
const engineLog = engineActionsInWindow(emitted, window);
// The fills that happened since the last vantage — a graded-bus projection over the same seq window,
// so an order that filled between wakes reaches the agent (kestrel-fud; the base `kernelOf` carries the
// OPEN-briefing `[]`, which is correct pre-open).
const fillsSinceLast = fillsInWindow(emitted, window);
const base = kernelOf(af, instrument, standing ?? {}, config.execMultiplier ?? 1);
// The sizing headroom (kestrel-m9i.32): the max fillable size the remaining-R budget admits for the
// exec instrument, so a model sizes WITHIN the bounded-risk cost basis (ADR-0017) rather than into a
// silent fire-time clamp. PURE — derived from the plan-lifecycle budget (`remaining $`) + the frame's
// exec spot + the exec multiplier (equity 1 / option 100). An option's per-leg premium is not sourced
// at this seam, so the option cue surfaces the remaining-$ ceiling + the basis rule (never a guess).
const sizing = sizingHeadroomOf({
instrument,
remainingUsd: base.budget?.remaining ?? null,
spot: af.spot,
multiplier: config.execMultiplier ?? 1,
});
const kernel = { ...enrichedKernelOf(base, dataHealth, engineLog, ctx, sizing), fillsSinceLast };
// The ARMED plans' enforced terms for the `armed-plan` pane (kestrel-wa0j.29) — captured from the armed
// ASTs the driver holds (`config.armedPlanAsts`), filtered to the plans this wake's kernel reports in an
// ENFORCED lifecycle state (armed/fired/managing). The watcher manages plans whose document text it
// never otherwise sees; the terms enter the frame THROUGH this typed input (the renderer invents none).
// Absent config, or no enforced plan ⇒ `undefined` (absent-not-hidden: the field is unserialized and the
// pane fails closed to absent-with-reason, byte-identical to a frame without it).
const enforcedPlanNames = new Set(base.plans.filter((p) => ENFORCED_PLAN_STATES.has(p.state)).map((p) => p.name));
const armedPlan = config.armedPlanAsts !== undefined
? armedPlanTermsOf(config.armedPlanAsts, enforcedPlanNames, config.sessionDate)
: undefined;
return {
// WakeDeltaInput
wakeIndex: af.n,
minutesSinceLast: Math.round((ctx.wakeTs - ctx.prevVantageTs) / 60_000),
timeToCloseMin: minutesToClose,
phase: mapPhase(af.phase),
clockET: clockOf(ctx.wakeTs),
wakeReason: ctx.wakeReason,
// kestrel-121: price the chain `fair` column through the injected `fairTauYears` at this wake's cutoff
// (`ctx.wakeTs`) — the SAME source the fill engine prices `@fair` with. Absent ⇒ `fair` stays `—`.
market: marketPaneOf(af, instrument, options, config.fairTauYears !== undefined
? { tauYears: config.fairTauYears, nowTs: ctx.wakeTs, ...(config.fairExpiry !== undefined ? { expiry: config.fairExpiry } : {}) }
: undefined),
kernel,
// The prior author vantage's served values (kestrel-wa0j.48) — threaded from the driver so the
// `delta` pane states what MOVED under stateless-redraw (the renderer invents no value; the prior
// data must enter through the frame input). Present only on a wake AFTER the first; absent ⇒ the
// field is unserialized and the frame is byte-identical to today (the delta pane fails closed).
...(config.priorVantage !== undefined ? { priorVantage: config.priorVantage } : {}),
// The prior sessions' tapes (kestrel-wa0j.20) — threaded from the driver so `tape d-N` serves
// ordinal N's rows as its own single-session block. Present only when the run injects a multiday
// tape; absent ⇒ the field is unserialized and the frame is byte-identical (Train 1B absence).
...(config.priorSessions !== undefined ? { priorSessions: config.priorSessions } : {}),
// The armed plans' enforced terms (kestrel-wa0j.29) — threaded so the `armed-plan` pane renders the
// WHEN/entries/exits/invalidation/size the watcher enforces. Present only on a wake with at least one
// plan in an enforced state; absent ⇒ the field is unserialized and the frame is byte-identical.
...(armedPlan !== undefined ? { armedPlan } : {}),
// acting-kernel strip
wakeOrdinal: ctx.wakeOrdinal,
// The stable replay identity (kestrel-5zl.19) — the source that raised this wake + its monotone
// per-source ordinal, the two halves `wakeKeyOf` joins the captured turns on.
wakeSource: ctx.wakeSource,
wakeSourceOrdinal: ctx.wakeSourceOrdinal,
// The relative-day coordinate (kestrel-5zl.16.4) — present ONLY for a multi-session run (it rides the
// date-blind author projection), absent + unserialized for a standalone session (goldens unchanged).
...(af.sessionOrdinal !== undefined ? { sessionOrdinal: af.sessionOrdinal } : {}),
severity: "routine",
deadline: {
minutesToClose,
minutesToNextWake: ctx.nextWakeTs === null ? null : Math.round((ctx.nextWakeTs - ctx.wakeTs) / 60_000),
},
dataHealth: { asOfClockET: clockOf(ctx.wakeTs), stale: [], unavailable: [] },
// The scheduled wake's stored View rides the delivery (kestrel-wa0j.4) — the adapter renders THIS
// wake's panes under it; absent ⇒ the field is unserialized and the frame is byte-identical to today.
...(ctx.view !== undefined ? { view: ctx.view } : {}),
attention: { wakesRemaining: ctx.wakesRemaining, coalesced: ctx.coalesced },
};
}
/** Fail-closed date-blind fence over the assembled acting Frame (the agent boundary): serialize with the
* SAME 1e-6 rounding the machine summary uses (so a raw float tail can't trip the epoch grep as a false
* calendar signal), then run the SHARED {@link scanDateLeak}. A leak is REFUSED, never delivered. */
function assertFrameDateBlind(frame) {
const body = JSON.stringify(frame, (_k, v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 1e6) / 1e6 : v);
const hit = scanDateLeak(body);
if (hit !== null) {
throw new Error(`simulate: acting Frame leaks a ${hit.name} (${JSON.stringify(hit.match)}) at the agent boundary ` +
"(fail-closed, EVALUATION contamination fence)");
}
}
// ─────────────────────────────────────────────────────────────────────────────
// runSimulateSession
// ─────────────────────────────────────────────────────────────────────────────
/**
* Drive one frozen-at-wake Simulate session through the {@link Agent} seam and return its graded
* {@link Blotter} + the {@link CapturedTurns} it consumed. Reuses the SAME {@link SessionCore} the stepped
* day drives (bus-stepping, Gate, `projectAuthorFrame`); the ONE behavioral change is that the wake set is
* answered by `agent.decide` rather than a file handshake. `async` because the live loop is deliberately
* non-deterministic; the recorded/fixed adapters resolve synchronously.
*/
export async function runSimulateSession(opts) {
if (opts.busPath === undefined && opts.events === undefined) {
throw new Error("simulate: runSimulateSession needs either `busPath` or `events`");
}
const events = opts.events !== undefined ? [...opts.events] : [...readBus(opts.busPath)];
const meta = requireMeta(events);
const specs = resolveSpecs(meta, opts.instruments);
const fairTauYears = opts.fairTauYears ?? expiryTauYears(meta.session_date);
const firstTs = events[0]?.ts ?? 0;
const lastTs = events[events.length - 1]?.ts ?? firstTs;
const sessionDate = meta.session_date;
// ── Clock-honest session semantics (ADR-0040 / kestrel-w7la.1, clock-honest wakes §§2, 4, 7) ─────
// The switch is the agent config's `clockHonest` (a ConfigId axis — a clocked run never shares a grid
// column with an unclocked one). EVERY precondition refuses LOUDLY at open — never a silent default,
// never a silent downgrade to latency-blind (§4).
const clockHonest = opts.agent.config.clockHonest === true;
if (clockHonest) {
const buf = opts.agent.config.latencyBufferMs;
if (typeof buf !== "number" || !Number.isInteger(buf) || buf < 0) {
throw new Error(`simulate: clock-honest run refused at open — \`latencyBufferMs\` must be a non-negative integer (got ${JSON.stringify(buf)}); there is NO default (never a silent default — a designed default would be a judge-designed weighting, ADR-0030 spirit). \`0\` is legal: it declares a colocated seat.`);
}
// Eligibility: the ADR-0040 data floor, declared with the tape and verified fail-closed (§4).
const ineligible = clockHonestIneligibility(opts.dataRung);
if (ineligible !== null) {
throw new Error(`simulate: clock-honest run refused at open — ${ineligible}`);
}
// A costless (pre-v6 / latency-blind) capture replays ONLY under latency-blind semantics (§7):
// replaying it clocked would silently change what the run claims to be. Probed at the OPEN key —
// the one entry every driver-captured recording holds.
if (opts.agent.recordedCostMs !== undefined && opts.agent.recordedCostMs(OPEN_WAKE_KEY) === undefined) {
throw new Error("simulate: clock-honest run refused at open — the recorded capture is COSTLESS (it carries turns but no measured deliberation costs); a costless capture replays only latency-blind (ADR-0040 §7, never a silent downgrade)");
}
}
/** The configured latency buffer, applied to every Seat-answered vantage (0 when latency-blind —
* never read on that path). */
const bufferMs = clockHonest ? opts.agent.config.latencyBufferMs : 0;
/** The injectable wall clock — consulted ONLY on the live clocked path (§7's one minting seam). */
const nowFn = opts.now ?? Date.now;
/**
* Measure — or, in replay, READ — one Seat turn's deliberation cost (§7: mint once, live-side, at
* THIS seam; the only minting site). Replay mode (an agent exposing `recordedCostMs`) never touches
* the clock — a replay path that re-times is a defect by definition (the poisoned-clock fixture).
*/
const clockedTurn = async (key, call) => {
const costSource = opts.agent.recordedCostMs?.bind(opts.agent);
if (costSource !== undefined) {
const turn = assertHandlerResponseTimeless(await call());
const c = costSource(key);
// `null` = the capture never held this key — the replay machinery SYNTHESIZED the turn
// (divergence stand-down / inserted-vantage pass). Engine-minted, not Seat deliberation:
// cost 0, no buffer, no record (§7 — charging tape for a turn no Seat took fabricates cost).
if (c === null)
return { turn, measuredMs: 0, seatAnswered: false };
if (c === undefined || !Number.isInteger(c) || c < 0) {
throw new Error(`simulate: clock-honest replay hit a costless/malformed capture entry at wake ${key} — a costless capture replays only latency-blind (fail-closed, ADR-0040 §7)`);
}
return { turn, measuredMs: c, seatAnswered: true };
}
const t0 = nowFn();
const turn = assertHandlerResponseTimeless(await call());
const t1 = nowFn();
return { turn, measuredMs: Math.max(0, Math.round(t1 - t0)), seatAnswered: true };
};
// Capture the a57.14 experimental envelope from the agent's config (kestrel-5zl.6) and stamp it on the
// graded META at open — the CAPTURE side of a57.14. Only an authoring config declares one; a NO-LLM
// Backtest (fixedPlan/recorded with BACKTEST_CONFIG) declares none ⇒ no stamp ⇒ the Blotter stays
// certified (fail-closed by omission — never a fabricated identity). A declared-but-incomplete identity
// is stamped so the projector fails the Blotter closed to PROVISIONAL (a57.14). The FULL ConfigId
// rides alongside under the SAME condition (m9i.2 owner tweak): the grid CellKey keys on it, so
// temperature-only config variants land in DISTINCT cells.
const envelope = envelopeForConfig(opts.agent.config);
const configId = envelope !== undefined ? deriveConfigId(canonicalizeConfig(opts.agent.config)) : undefined;
// The cell config axis (fillSeed stripped — ADR-0016 §3): stamped ONLY when it DIVERGES from configId (the
// config carried a fillSeed) — a seedless run's cell axis equals configId, so no stamp and the graded bus
// stays byte-identical. Seed-only variants pool as replicates in ONE cell; configId keeps the seeded RUN id.
const cellConfigId = envelope !== undefined ? cellConfigIdOf(opts.agent.config) : undefined;
const cellConfigStamp = cellConfigId !== undefined && cellConfigId !== configId ? cellConfigId : undefined;
// Arm the seeded sampled fill channel when the agent's config declares a fillSeed (kestrel-9gu.8.3,
// ADR-0016); a Backtest config (or any omitting the seed) leaves the sampler OFF (fail-closed).
const fillSeed = fillSeedOf(opts.agent.config);
// The sampled-channel qualification claim (kestrel-9gu.12), read fail-closed off the config: absent —
// every config until the 9gu.8.6 study passes — the headline gate resolves to the strict-cross floor.
const sampledQualification = sampledQualificationOf(opts.agent.config);
const openingCarry = opts.openingCarry;
// The wake-frontier floor is a FUNCTION of the cell's timescale band (kestrel-5zl.16.7). Read the band
// once from the opening carry (an absent carry — a standalone / flat open — is `emptyCarry().band` =
// `day`, so this run is byte-identical to today), resolve the pure floor, and use its three controls
// wherever the frontier gates a wake (`popNext` spacing/ceiling, `armStaleness` backstop, the own-fill
// spacing). The DAY floor reproduces today's exact constants — no churn.
const floor = wakeFloorForBand((openingCarry ?? emptyCarry()).band);
const core = new SessionCore({
meta,
specs,
fillModel: opts.fillModel,
rUsd: opts.rUsd,
fairTauYears,
firstTs,
// NO `tapeRegimeScopes` pre-scan here, DELIBERATELY (kestrel-ocyf round 3): this is the
// agent-interactive path (day.ts drives through it), and a whole-bus pre-scan is a LOOKAHEAD
// ORACLE — a mid-session supersede→arm would receive a notice grade that is a function of
// post-vantage events (definitive notice = "never written all day"; silence = "a REGIME write is
// coming"), and the REFUSAL_MARKERS seam pipes that onto the acting agent's frame, letting it
// read tomorrow's regime vocabulary through throwaway supersedes. Violates this file's own
// charter (no post-cutoff leak / no look-ahead) and the eval contamination firewall. Interactive
// sessions take the CONDITIONAL wording — derived only from `#tags` (events ingested so far),
// causally honest at any vantage, still loud at arm. The batch-replay `runSimSession` (a post-hoc
// report with no author to leak to) keeps the definitive pre-scan.
...(opts.makerFairParams !== undefined ? { makerFairParams: opts.makerFairParams } : {}),
...(envelope !== undefined ? { envelope } : {}),
...(configId !== undefined ? { configId } : {}),
...(cellConfigStamp !== undefined ? { cellConfigId: cellConfigStamp } : {}),
...(openingCarry !== undefined ? { openingCarry } : {}),
...(fillSeed !== undefined ? { fillSeed } : {}),
...(sampledQualification !== undefined ? { sampledQualification } : {}),
// The clock-honest attestation input (ADR-0040 §5): every §4 precondition already held above (the
// run refuses at open otherwise), so a constructed clocked core is an attested one. `true` or
// absent — the driver NEVER stamps `false`.
...(clockHonest ? { clockHonest: true } : {}),
});
const instrument = core.execSpec.symbol;
const instruments = meta.instruments.map((i) => i.symbol);
// DAY-COMPAT MODE (kestrel-5zl.3): the caller-owns-cadence path `runDaySession` drives through. When set,
// the driver reproduces the stepped runner's cadence + byte stream EXACTLY — the day's own structural wakes
// are the coverage, so NO platform staleness/backstop wake is injected (Divergence #1), and the CONTROL
// supersede carries