/** * The four arms, behind one interface, so the benchmark can compare them. * * V1 frontier compress only what arrives after the last cache * breakpoint; elisions name a readable path * V2 speculative V1, plus re-inflate a span the model's last reply shows it * is about to need -- on a request that was happening anyway * V3 history V1's markers, applied behind the frontier too * ccr a faithful imitation of HeadRoom: opaque hash markers, an * injected retrieval tool, an injected system message, and * history compressed. The control we have to beat. * * The ccr arm exists so the comparison is against their DESIGN rather than * against their marketing. It is implemented honestly -- if it wins, it wins, * and the plan says to stop and report rather than ship the proxy. */ import type { Tuning } from './options.js'; import type { EmbeddingCache } from './embedding.js'; import { type Finding } from './knowledge.js'; import { type AnchorDecision, type AnchorStore } from './anchor.js'; import { type ProviderRequest } from './frontier.js'; import type { Elision, Stamp } from './types.js'; export type StrategyName = 'v1-frontier' | 'v2-speculative' | 'v3-history' | 'v4-substitute' | 'ccr'; export interface StrategyOptions { /** * The key every marker this strategy writes will carry, when the caller picks it. * * LEAVE IT OUT IN PRODUCTION. Omitted, each block is stamped with a keyed MAC * over its own content, which is what makes a planted marker-shaped line * unforgeable: the secret lives in `annotate.ts`, is minted once per process * and is never emitted, so the author of the content cannot compute the key a * decoder will honour. Pinning a value here does not expose that secret and * does not let content predict a stamp -- it only lets a caller who already * controls the whole request choose what its own markers authenticate as. * * `null` means `do not stamp`, and yields markers nothing will honour. The * comparator wants exactly that for the arm it publishes, and wants a pinned * value for the arm it measures, because a MAC over a secret that changes * every process makes the measured text change every run. */ readonly stamp?: Stamp; /** Writes content with no file of its own somewhere readable. */ readonly spill?: (content: string, hint: string) => string; /** * V2 only: spans the model's previous reply suggests it is about to need. * Supplied by the caller so this stays a pure function of its inputs. */ readonly wanted?: readonly string[]; /** * Memory of which conversations we have already re-anchored. * * Supplied by the caller rather than held here, so this module keeps no * state of its own and two concurrent requests cannot observe each * other's -- HeadRoom's #3486 is exactly that bug. Absent means V1 behaves * as it always has and never touches the cached prefix. */ readonly anchors?: AnchorStore; /** * What this project already established, for the cached prefix. * * Supplied by the caller rather than read here, because reaching for a * graph on disk would make a pure function of (request, options) into * something that depends on the filesystem. Absent means nothing is * injected, which is the default: this is payload, and it has to be * asked for. */ readonly findings?: readonly Finding[]; /** True when `findings` came from a graph shared across projects. */ readonly sharedGraph?: boolean; /** * Inject no knowledge block on this pass, whatever the graph holds. * * PASSING NO FINDINGS IS NOT ENOUGH, which is the whole reason this exists. A * block is composed on the turn the prefix is free and then REMEMBERED, and * every later turn replays it from the anchor record so the cached prefix stays * byte-identical -- so a caller that drops `findings` still gets the block back. * The net-saving guard needs a pass that genuinely sends none, to compare * against, and this is that pass. */ readonly suppressKnowledge?: boolean; /** Characters of findings allowed in the prefix. */ readonly knowledgeBudget?: number; /** * Vectors a request-level pre-pass already computed. * * Supplied by the caller, never built here: embedding is async and these * strategies are synchronous all the way down. See `embedding.ts`. */ readonly embeddings?: EmbeddingCache; /** * The resolved dials. * * Resolved by the CALLER and held fixed for the life of a proxy, because * changing a dial mid-session changes how the cached prefix compresses -- * and a prefix that changes is a cache miss on everything. */ readonly tuning?: Tuning; } export interface StrategyResult { readonly request: ProviderRequest; readonly elisions: readonly Elision[]; /** Tokens of preamble this strategy ADDED to the request. */ readonly injectedChars: number; /** * The authenticators the blocks in this request were compressed under. * * ONE PER BLOCK, NOT ONE PER REQUEST, and the difference is the provider's * cache. A stamp is a keyed MAC over the block's own content, so a block that * arrives unchanged next turn compresses to the same bytes and the cached * prefix still matches; a stamp derived from the whole request would change on * every turn and rewrite every marker behind the breakpoint. * * Returned because the decoder honours only markers that verify: output handed * over without these cannot be inverted by anyone, and `lossless` is a claim * about inverting it. */ readonly stamps: readonly Stamp[]; /** * What to remember about this conversation IF this request is the one sent. * * Uncommitted on purpose: a caller that decides not to use the rewritten body * must not leave a record saying it did. Commit it with * `anchors.remember(anchor.key, anchor.record)` once the body is accepted. */ readonly anchor?: AnchorDecision; } /** * What the agent is asking, read off the request itself. * * Only SHORT blocks count. The last message in an agentic conversation is * usually a tool result -- tens of kilobytes of the very content being * compressed -- and tokenising that as the question would make every block * maximally relevant to itself. That failure would look like it was working, * which is the worst kind. */ export declare function questionIn(request: ProviderRequest): string; /** * The steering text for TOOL deferral, which must not move during a session. * * WHY THIS IS NOT `questionIn`. Both pick what is relevant, but they rewrite * different halves of the request and only one of them is cached. Content * compression works AFTER the cache frontier, so it may follow the live * question and change every turn at no cost. Tool deferral rewrites the tools * array, which sits at the FRONT of the prefix -- so if its steering text * changes, the chosen tools change, the prefix changes, and every cached token * behind it is invalidated. * * MEASURED, and it is not a small effect. Steering deferral with `questionIn` * kept a stable COUNT of 14 deferred tools while producing 11 distinct * `deferredToolChars` values across 41 requests -- the same number of tools, * but a different set each turn. Cache creation went from 2,188 tokens per * request to 7,048 while cache reads fell from 30,307 to 14,181: weighting * writes at 1.25x and reads at 0.1x, that is 5,766 -> 10,228, so the feature * that removes 38,322 characters per request made the bill 1.77x WORSE. * * The first user turn is the task, and the task does not change while it is * being worked on. Relevance to it is what tool selection actually wants. */ export declare function taskIn(request: ProviderRequest): string; /** * The share a rewrite must remove to repay itself, for this proxy's prior. * * FIXED FOR THE LIFE OF A PROXY, like every other dial, and that is the whole * point: a threshold that moved with the conversation would decline a rewrite * on one turn and accept it on the next, re-sending the entire prefix at 1.25x * instead of re-reading it at 0.1x. */ export declare function minRewriteShare(tuning?: Tuning): number; /** * The exact share a rewrite must remove to repay itself over `turns` more turns. * * NOT `minRewriteShare`, and deliberately a second function rather than a * correction to that one. `minRewriteShare` is `W/R/turns`, a first-order * approximation that is honest at the hundred-turn prior it was written for and * nonsense below about twelve -- at five turns it asks a rewrite to remove 250% * of the payload. The speculative path below bets on a deliberately short * horizon, so it needs the exact form; every existing caller keeps the dial it * was measured against. * * Rewriting a prefix the provider already holds costs `r * (W + R*N)`, where `r` * is the share that survives compression, against `R * (N+1)` for leaving it * exactly where it is. Solving for the removed share `1 - r` gives this. */ export declare function breakEvenRewriteShare(turns: number): number; /** * How many more turns a JOINED conversation is assumed to have left. * * MEASURED, NOT CHOSEN, AND SET TO THE WEAKEST CONVERSATION RATHER THAN THE * AVERAGE ONE. Joining mid-conversation is the one case where the provider * demonstrably holds a prefix in its ORIGINAL form, so a rewrite that does not * pay is money spent for nothing, and the loss is one-sided: betting too low * refuses a rewrite and forfeits a saving, betting too high buys a cache write * nobody re-reads. A bet with that shape belongs at the floor of the * distribution, not at its middle. * * `bench/subscription/session-horizon.mjs` re-derives the floor. It groups every * request in the local transcripts by conversation and takes each conversation * ENTIRE -- a time window truncates the ones that straddle its edges and * manufactures low outliers, inventing a conversation at 37.5 that does not * exist -- then divides each one's cache reads by its cache writes. That ratio * is, by the cost model's own definition, the number of turns a written token is * re-read over. The census below is that measurement; `SESSION_HORIZON_CENSUS` * carries its provenance and `bench/compression/cost-model.mjs` documents the * same derivation for its pooled sibling. * * Forty is the floor of fourteen conversations, 41.1, rounded DOWN -- the only * direction a floor estimated from fourteen samples can be moved without * risking the thing it protects. It sets the bar at 21.9%, against the 65.7% * that the previous unmeasured five demanded. * * WHAT THE CORPUS CANNOT SEE. It is one user's agentic transcripts, and the * smallest conversation in it is 140 requests, so a three-turn chat -- the case a * high bet hurts most -- is not represented. What the corpus does show is that * length barely predicts horizon within it: the shortest conversation, 140 * requests over 1.4 hours, has nearly the HIGHEST ratio at 93.8, because a short * session still re-reads its whole prefix on every turn. A population that * disagrees overrides this through `tuning.assumedSessionTurns`. */ export declare const JOINED_TURNS_ASSUMED = 40; /** * The measurement `JOINED_TURNS_ASSUMED` is read off, frozen with its provenance * so the constant above is checkable rather than merely asserted. Re-derive with * `node bench/subscription/session-horizon.mjs`; nothing in the shipped path * reads this, and it deliberately does not import from `bench/`. */ export declare const SESSION_HORIZON_CENSUS: Readonly<{ measuredOn: "2026-09-29"; basis: "whole conversations, sidechains excluded, no time window"; conversations: 14; requests: 45042; /** Reads per written token, per conversation. The floor is what the bet uses. */ horizon: Readonly<{ floor: 41.1; p10: 44.8; p25: 59.7; median: 69.2; pooled: 71.5; }>; smallestConversation: Readonly<{ requests: 140; spanHours: 1.4; horizon: 93.8; }>; }>; export declare function v1Frontier(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** * V2: V1, plus re-inflation of spans the model is about to need. * * MEASURED, NOT ASSUMED. This guesses, and a wrong guess spends tokens * re-inflating something unwanted. It is compared against V1 on the same * fixtures; if it loses, V1 ships. */ export declare function v2Speculative(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** V3: V1's markers with no frontier restriction. */ export declare function v3History(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** * The CCR-style control: what HeadRoom does, as faithfully as we can state it. * * Opaque markers, history compressed, and the two injections that make the * markers redeemable. `injectedChars` is what those cost -- the number their * published reduction figures do not appear to include. */ export declare function ccrStyle(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** * V1, plus the history substitution -- the one region nothing else touches. * * COMPOSED RATHER THAN FORKED. V1 attacks the fresh tail and the tool * definitions; this attacks the reasoning in history, which is 52% of it and * which every other arm steps over because `messageIsSigned` forbids rewriting * it. They are disjoint, so the substitution runs first and V1 then does * exactly what it always did to what is left. Anything V1 learns about * anchoring, knowledge injection and the saving floor is inherited rather than * reimplemented, which is the difference between a fifth arm and a second * codebase. * * ORDER IS NOT ARBITRARY. Substitution must happen BEFORE the anchor decision, * because the anchor records what the cached prefix looks like and it has to * record the prefix we actually send. Running it afterwards would anchor one * body and transmit another -- the same class of defect as recording * `anchored: true` for a rewrite the proxy then discarded. * * OFF BY DEFAULT AT THE CALLER. Registered here so it is measurable; the proxy * gates it on `TOKEN_OPTIMIZER_PROXY_SUBSTITUTE`. Deferral shipped default-off * and was therefore never measured for months, so the switch is deliberate and * so is the instrumentation behind it. */ export declare function v4Substitute(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** Every arm, by name. */ export declare const STRATEGIES: Record StrategyResult>; //# sourceMappingURL=strategy.d.ts.map