import type { Message, ToolCall } from '../../types/message/index.js'; import type { FileReadTracker } from '../../types/tool/index.js'; /** * Reconstructing a file's body from calls the conversation can still see. * * One module because two callers need the same answer and must not each have * their own idea of it: the derived work context asks "is this ledger entry's * body one the model can rebuild from what is in front of it?", and the resume * seed asks "which of these ledger entries can be rebuilt at all?". A * predicate that admitted a hop in one and refused it in the other would mean * a path referenced as evidence that the ledger never witnessed, or the * reverse. Everything below is a function of the messages, the ledger and the * bounds — there is no filesystem access anywhere in this file. */ /** * Edit CALLS one chain may carry before the path is withheld. * * On calls rather than replacements because the bound is on replay work, and * an `edits: [...]` batch is one pass over the content however many hunks it * holds. Eight is generous for the shape this exists for — a file written and * then refined within a turn — and short enough that the whole request's * replay stays a rounding error beside the model call it rides on. */ export declare const MAX_EDIT_CALLS = 8; /** * Content a replay may materialise across every path in one pass. * * Counted cumulatively in UTF-16 units — the measure the caps below and the * step-context budget already use — and charged for the write body each chain * starts from as well as every body an edit builds on top of it, because those * are the strings actually built. Each operation is measured exactly against * the content it is about to be applied to and refused before it is built, so * a pass whose history is full of long chains it is going to refuse anyway * cannot make itself slow proving that. A refusal is charged nothing: the body * was never built, and the paths after it keep their room. */ export declare const MAX_REPLAYED_UNITS: number; /** * Path lengths a `write` entry may go out with. * * Exported because `file-evidence-context.ts` puts the same bound on the * spelling a `read` entry emits — a read is the other thing that puts a path * in the work-context message, and the constant belongs to this module. */ export declare const MAX_PATH_UNITS = 512; /** * One pass's answer to "which file did this call touch", memoised. * * Keyed on the call OBJECT rather than on its id: an id claimed by two calls * is an ambiguity this module refuses to settle by position, and settling it * by cache hit instead would be the same mistake wearing a different hat. A * `WeakMap` because the entries are worth exactly as long as the transcript * they describe, and the value is the declared path or `null` for a call that * declares none — `undefined` means only "not asked yet". */ export type PathAttributions = WeakMap; export declare function createPathAttributions(): PathAttributions; /** * The ledger key a tool-call path belongs to, or `undefined` for a path this * pass may not key at all. * * A resolver rather than a working directory, because the two callers do not * key alike and must not be made to. The projection READS a ledger the tools * wrote and looks entries up lexically, as it always has: a spelling that does * not match the tool's canonical key simply finds nothing. A seed WRITES that * ledger, so its keys have to be the ones the tools will come looking for — * which means resolving a path the way `write` and `edit` resolve it, through * every symlink, and that is filesystem work nothing in this module is allowed * to do. So the caller resolves first and hands the answers in. */ export type FileKeyResolver = (path: string) => string | undefined; /** * The lexical key space: the path resolved against the working directory, or * the path as written when the tools address a sandbox. * * What a reader of the ledger uses. Not what a writer of it may use: see * {@link FileKeyResolver}. */ export declare function lexicalFileKeys(workingDirectory: string, sandboxed: boolean): FileKeyResolver; /** The calls and receipts of one conversation, indexed by call id. */ export interface VisibleHistory { /** `null` where an id was claimed by more than one assistant call. */ readonly calls: ReadonlyMap; /** `null` where an id was answered more than once. */ readonly results: ReadonlyMap; /** The ledger key a tool-call path resolves to. */ readonly keyOf: FileKeyResolver; } /** * Index a transcript once, so every lookup below is a map hit. * * An id claimed twice, or answered twice, indexes to `null` rather than to * whichever message came last: two calls wearing one id is an ambiguity, and * resolving it by position would let the second silently vouch for the first. */ export declare function indexVisibleHistory(messages: readonly Message[], keyOf: FileKeyResolver): VisibleHistory; /** * Every path a replay of this history might have to key, in the order the * calls arrived. * * For a caller that has to resolve them before the replay can run, and only * for that: nothing here decides anything about a path. Empty when the history * holds no `write` and no `edit` call at all, because a claim is only ever made * by a mutation — a conversation that only read files reconstructs nothing, and * resolving its reads would be filesystem work with no possible result. * * Every mutation the walk can ATTRIBUTE has to appear here, the ones it will * refuse to reconstruct included. This used to drop a call whose arguments ran * past the evidence bound, so the walk reached it holding no key for its path, * read that as a mutation it could not attribute, and abandoned the seeding * entirely: one thirty-kilobyte `write` anywhere in a conversation erased every * witness in it. The bounds belong to what may be believed; the path is read * here out of the same field the walk reads it from, under no bound at all. */ export declare function collectObservedPaths(messages: readonly Message[], attributions?: PathAttributions): readonly string[]; /** * The visibility every hop must pass, write or edit, root or tip. * * One function because a middle hop is not a lesser claim than the last one. * A chain whose second edit was cleared by compaction, or came back an error, * or arrived truncated, reconstructs nothing at all — so the whole path is * withheld rather than emitted as the part still visible, which would name a * body the model cannot rebuild. * * A call a `pre_tool_use` hook SKIPPED is the one refusal that does not arrive * as an error. The hook declined the call; nothing failed, so the receipt is a * plain success carrying the sentence `plugin-hooks.ts` writes for it, and * reading that as a `write` would hand back a body the tool was never allowed * to put on disk — a fingerprint for a file that still holds whatever it held * before, and a spurious drift refusal on the next edit. Recognised through * the same function that writes the sentence, so the two cannot drift apart. */ export declare function visibleCall(history: VisibleHistory, id: string, name: 'write' | 'edit' | 'read'): ToolCall | undefined; /** A full body that arrived whole in one visible call. */ export declare function visibleWrite(history: VisibleHistory, id: string): { readonly path: string; readonly key: string; readonly body: string; } | undefined; /** * One visible edit call's arguments, ready to replay. * * No length bound on the path here, unlike the write above. A write's path is * the spelling an entry goes out with; an edit is held to the KEY it resolves * to, which its chain's root has already been bounded on. */ export declare function visibleEdit(history: VisibleHistory, id: string): { readonly key: string; readonly input: unknown; } | undefined; /** Room left for content this pass may still materialise. */ export interface ReplayBudget { remaining: number; } export declare function createReplayBudget(units?: number): ReplayBudget; /** * Charge one body against the pass's replay allowance. * * Checked before it is taken, so a refusal costs nothing. Deducting first and * reporting afterwards left the allowance negative, and one oversized path * then refused every admissible path behind it in the same pass. */ export declare function spend(budget: ReplayBudget, units: number): boolean; /** * Replay one edit call onto the body in hand, within what the budget has left. * * `undefined` for a hop the budget turns away and for one that no longer * applies, because both mean the same thing to every caller here: this path * reconstructs nothing. The charge is settled either way — what the attempt * built, it built — and for a hop refused at its first operation that charge * is zero. */ export declare function replayHop(content: string, input: unknown, budget: ReplayBudget): string | undefined; /** * Whether a refused mutation has reported this path stale since the ledger * last observed it. * * That refusal read the real file to make its comparison, so the ledger knows * its body is behind disk without a consumer here touching the filesystem. * The reconstruction would still be a body the model can rebuild — but not the * FILE's body, which is what an entry claims. Withheld until a real * observation re-baselines the ledger, which is also what clears the flag. */ export declare function knownStale(tracker: FileReadTracker, key: string): boolean; /** * Paths one pass may hold a reconstructed body for at a time. * * The projection emits at most this many entries and keeps the most recent, so * a pass that tracked more would be doing work whose result is discarded. */ export declare const MAX_WITNESSED_PATHS = 6; /** What one pass of {@link replayObservationLedger} established. */ export interface LedgerReplayReport { /** Paths whose body was reconstructed exactly, with a fingerprint and a witness. */ readonly pathsWitnessed: number; /** * Paths the conversation demonstrably mutated but whose body could not be * rebuilt. Nothing is written to the ledger for these; see {@link commit}. */ readonly pathsSeen: number; /** * Content materialised, in UTF-16 units. Never above * {@link MAX_REPLAYED_UNITS}, and non-zero even for a pass that established * nothing: it reports the work done, not the work kept. */ readonly unitsReplayed: number; } /** * Rebuild an observation ledger from a conversation's own history. * * The ledger is process state. A resumed conversation gets a fresh empty one, * and until something reads a file again the runtime knows nothing about files * this conversation wrote in full — so the projection admits nothing and the * model re-reads a body already in front of it, every time a session is picked * back up. * * What is reconstructable here is exactly what the projection would admit, by * the same predicates and the same bounded replay, because both go through the * functions above. A `write` whose call and successful receipt are both intact * hands back its body and its witness; the edits on top of it are replayed * hop by hop and extend the chain. Anything else about a path the conversation * mutated — a receipt compaction cleared, a call the transcript never answered, * a mutation it refused, one a `pre_tool_use` hook skipped before it ever ran, * a hop that no longer applies, a body past the bounds, a call too long to * quote back — withdraws whatever this pass was holding for that path and * writes NOTHING for it. * * Content-backed observations only, and that is the whole of what a resume * restores. A path in the ledger with no fingerprint is a path `write` lets * through unread: the read-before-overwrite refusal is `hasRead`, and the drift * comparison it guards needs a body to compare. Seeding membership alone would * therefore admit a full overwrite of a file that changed while the session was * closed, with nothing checked — weaker than the empty ledger a resume used to * start from, which refuses that overwrite outright. So a path this pass cannot * rebuild is left exactly as that empty ledger leaves it, and the first real * read re-establishes it. * * Two things this deliberately does not do. It never reads the filesystem: a * fingerprint restored here is a claim derived from history, and the built-in * mutation checks still compare it against the real file before anything is * written — so a file changed while the session was closed is refused exactly * as it is today, and the refusal's drift flag withdraws the entry. And it * never recovers a body from a `read` receipt by undoing the line numbering: a * read's rendering is compared, whole, against a body this pass already holds, * and a read that cannot be matched that way withdraws the body rather than * supplying one. * * That first guarantee is the reason `keyOf` is a parameter. Everything written * here has to land on the key the mutation tools will look it up under, and on * a host those keys are canonical — `write` and `edit` follow every symlink * before they touch the ledger. Keying a seed lexically instead writes entries * into a key space the tools never read: the projection would then match a * fingerprint this pass had just written to it, and the drift refusal that is * supposed to withdraw the claim would land somewhere else and never reach it. * So the caller resolves the paths the way the tools do — see * `file-evidence-seed.ts` — and a path it could not resolve gets no key, which * makes the mutation that named it unattributable and stops the pass. */ export declare function replayObservationLedger(messages: readonly Message[], tracker: FileReadTracker, keyOf: FileKeyResolver, attributions?: PathAttributions): LedgerReplayReport; //# sourceMappingURL=file-evidence-replay.d.ts.map