/** * Substituting conversation history with what it was carrying. * * History is ~65% of a live request and the only part that grows every turn. * Measured on a real 27-turn THOL conversation (60,274 chars): signed * `thinking` 31,585 (52%), tool results 21,706 (36%), `tool_use` 5,128 (9%), * assistant text 1,817 (3%). Everything this project optimised before now * attacked the other 35%. * * WHY THIS IS NOT THE PROBE THAT ALREADY LOST. `server.ts` carries a * thinking-drop probe measured at 1.01/0.71/1.32/0.72 against control, beaten * by deferral alone on three of four tasks. It REMOVES. This SUBSTITUTES, and * the two differ in the one place the bill is decided: * * - Removal deletes a signed block and, where that block was the message's * only content, deletes the message. Message count moves, indices shift, * and three existing tests that encode the 1:1 invariant have to be * rewritten to allow it. * - Substitution edits the message IN PLACE, leaving whatever already showed * what the turn did and writing a marker only when nothing would remain. * Message count is preserved, no index moves, and the 1:1 invariant is * never broken -- so the constrained tests need no weakening, which is the * outcome the plan wanted and could not get. * * THE CACHE ARGUMENT, WHICH IS THE WHOLE DESIGN. A prefix rewrite costs a full * 1.25x write on everything kept, against 0.1x to re-read it untouched -- * break-even `11.5 * after / removed` turns. The probe's first mode exempted * the newest assistant turn, which put a boundary in the prefix that MOVED * every turn: message A was sent whole on the turn it arrived and rewritten on * the next, so the prefix was rewritten on EVERY request. That is visible in * its numbers -- code-debug-pipeline-py took 71% fewer turns and still cost 10% * more, so per-turn cost had roughly quadrupled. * * The rule that avoids it entirely: TRANSFORM EVERY MESSAGE THE FIRST TIME IT * IS SEEN, AND NEVER RECONSIDER. Then turn N's prefix is turn N-1's prefix plus * new content, the provider's cached copy still matches, and the substitution * costs no rewrite at all -- not "a rewrite that repays in seven turns", none. * That is why `substituteHistory` is a pure function of each message alone. It * takes no floor, no turn count and no conversation-level state, because any of * those would make the output of message `i` depend on something that changes, * and a prefix that changes is a cache miss on everything before it. * * WHAT GOES BACK IN: ALMOST NOTHING, AND THAT IS THE POINT. A finding in the * graph is PROJECT knowledge -- durable, cross-session. History carries TASK * STATE: which files this session already edited, which command already failed. * No finding records that, so project findings cannot stand in for it. * * But the state is ALREADY in the message and is already being kept. The * `tool_use` blocks name the tool and its target; the `text` block is the * model's own conclusion. An earlier version of this file synthesised a digest * naming those same tool calls, which restated what the model could already * see and cost real bytes to do it -- measured at 312 of 312 messages on one * real session. So the substitute is written only where removal would leave an * empty message, and everywhere else the answer is silence. * * A NOTE ON WHAT THE OFFLINE NUMBERS MEASURE. Session transcripts store * `thinking: ""` and keep only the ~480-byte signature, so a replay over them * prices the removal of SIGNATURES, not of reasoning text. Live requests carry * the text as well. Every figure derived from a transcript replay is therefore * a lower bound on what this saves in production. */ import { type Message } from './frontier.js'; export interface SubstitutionOptions { /** * Compresses one tool-result body. MUST be a pure function of its input. * * INJECTED RATHER THAN IMPORTED, for two reasons. It keeps this module free * of the engine registry, so the substitution can be tested without one. And * it makes the purity requirement a visible part of the contract instead of * something a caller has to infer -- a compressor that consulted the current * question, the conversation length or a spill sink would produce a different * body for the same block on a later turn, break the prefix at that message * and cost a full rewrite of everything after it, every turn. * * That is precisely why `v1Frontier` refuses to compress behind the cache * frontier: its compression IS query-dependent on fresh content. Applied from * first sight with the query omitted, the same engines become safe to use * everywhere. */ readonly compressToolResult?: (text: string) => string; } export interface SubstitutionResult { /** The rewritten messages. Always the same length as the input. */ readonly messages: Message[]; /** Characters of reasoning removed. */ readonly removedChars: number; /** Characters of digest written back in their place. */ readonly substituteChars: number; /** How many messages were substituted at all. */ readonly substituted: number; /** Characters removed from tool results, which are their own region. */ readonly toolResultChars: number; } /** * Replaces model reasoning in history with a digest of what it produced. * * PURE PER MESSAGE. The result for message `i` depends on message `i` and * nothing else -- not on how long the conversation is, not on where the cache * frontier sits, not on which turn this is. That is the property that makes the * transform free rather than merely profitable: applied from the first turn a * message appears, it never rewrites a prefix the provider has already cached. * * ONLY ASSISTANT MESSAGES, and only their reasoning. A `tool_result` lives in a * user message and is left entirely alone here; compressing those is a separate * concern with a separate risk profile, and mixing them would make a regression * unattributable to either. */ export declare function substituteHistory(messages: readonly Message[] | undefined, options?: SubstitutionOptions): SubstitutionResult; //# sourceMappingURL=history.d.ts.map