import type { StatusLineStdin } from '../skills/skill-statusline-service.js'; export declare const HARNESS_CONTEXT_WITNESS_FILE = "harness-context-witness.json"; export declare const WITNESS_SCHEMA_VERSION = 2; /** * Contributor 1 of the budget: the harness reports a rounded percentage. * "Pre-calculated percentage of context window used" is not documented as * fractional, so the conservative reading is an integer percent, i.e. up to * ±0.5 percentage points = ±0.005 of the window. If the real payload turns out * to carry decimals this term shrinks tenfold and the guard gets sharper; the * assumption is visible in the first witness file, where * `usageTokens / modelWindowTokens` and `usedPercentage` must agree. */ export declare const WITNESS_PERCENT_ROUNDING_FRACTION = 0.005; /** * Contributor 2: the two sides do not sum exactly the same token quantities. * MEASURED, not guessed — on 2026-09-13 the harness's own pre-compact count * (963,306 tokens) exceeded peaks-loop's transcript estimate (961,658) by * 1,648 tokens = 0.171%. Used as a fraction of the tokens, not of the window. */ export declare const WITNESS_NUMERATOR_FRACTION = 0.0017; /** * The smallest window difference this guard claims to detect, and therefore * the yardstick for "is this sample sharp enough to answer at all". * * 3% is not arbitrary: the harness compacts a native-1M model at ~967,000 by * default while peaks-loop writes the model ceiling, so 1,000,000 vs 967,000 — * a 3.3% difference — is the smallest real-world disagreement between the two * denominators today. Anything the guard reports as "agree" while its own * budget is wider than that difference is a claim it cannot support. * * NOTE (repair cycle 2): the constant is a floor on the SAMPLE, and the sample * it floors is the WITNESS's, not peaks-loop's — a gap of this size leaves a * residual proportional to the witness's ratio, so a stale (or low) witness * shrinks the signal while the budget's rounding term does not shrink with it. * See `sampleSupportsAgreement`, which is where this constant is applied. At * zero skew it was already correctly calibrated (measured onset 0.176 of the * window against a predicted 0.177); the fault was that only zero skew was * correctly calibrated. */ export declare const MIN_DETECTABLE_WINDOW_DIFFERENCE = 0.03; /** * Which of the two in-range readings of a raw percentage was taken, or that * neither could be taken. `unestablished` is not a reading: the payload said a * percentage and carried nothing that could settle which scale it is on. */ export type PercentageUnit = 'fraction' | 'percent' | 'unestablished'; /** * A harness context snapshot, as written to * `/.peaks/_runtime//harness-context-witness.json`. * * The path is peaks-loop's own session runtime directory — deliberately NOT * the harness's settings file or any other harness-owned location. */ export interface HarnessContextWitness { readonly schemaVersion: number; /** When peaks-loop received the render payload. */ readonly capturedAt: string; /** * `context_window.used_percentage`, normalised to 0..1. `null` when this * render's payload had no percentage this module could read — the record is * still written, because "a render happened and carried nothing usable" is * a different fact from "nothing rendered", and the two are told apart by * exactly this file existing or not. */ readonly usedPercentage: number | null; /** * The payload's `used_percentage` VERBATIM, before any normalisation. Kept * so a human can tell a unit misread from a real disagreement: the number * above is a 0..1 fraction and this one is not, and only this one shows what * the harness actually said. */ readonly usedPercentageRaw: number | null; /** Which reading `usedPercentage` was normalised from, or why none was taken. */ readonly usedPercentageUnit: PercentageUnit | null; /** `context_window.context_window_size`. Recorded, never the comparison's denominator. */ readonly modelWindowTokens: number | null; /** * Sum of the prompt-side `context_window.current_usage` components. This is * the ALIGNMENT KEY: comparing it with a probe's `rawTokens` is how the * guard learns whether the two numbers describe the same API response, and * therefore how much of any difference is sampling skew rather than a real * disagreement. `output_tokens` is excluded so the sum is the same quantity * peaks-loop's transcript estimate sums. */ readonly usageTokens: number | null; /** The harness session id from the payload, for the foreign-session check. */ readonly outerSessionId: string | null; } export type WitnessVerdict = 'absent' | 'foreign-session' | 'unverifiable' | 'agree' | 'disagree'; /** * Why there is no witness to compare. The first two leave the same file system * state — no file — so the caller, which knows the session directory, resolves * which applies and this module turns it into a sentence. * * The third (`unreadable`, repair cycle 3) is decided by the READ, not by the * directory, and it is the reason `readHarnessWitness` returns a tagged union * rather than `null`: a file that exists but cannot be used is evidence that a * render DID happen, so calling it `not-rendered` names the wrong cause — the * exact mis-attribution §17-B was written to remove. */ export type WitnessAbsentCause = 'session-dir-missing' | 'not-rendered' | 'unreadable'; export interface HarnessWitnessComparison { readonly verdict: WitnessVerdict; /** Why the verdict is what it is, when it is not `agree` / `disagree`. */ readonly reason: string | null; readonly harnessPct: number | null; readonly peaksRatio: number; /** `peaksRatio - harnessPct`; `null` when the harness side is unknown. */ readonly deviation: number | null; /** * The quantity the verdict is actually decided on: `deviation` minus the * sampling skew the two token snapshots measure. Equal denominators put this * at zero no matter how stale the witness is, so a non-zero residual is the * window difference itself rather than the sample's age. */ readonly residual: number | null; /** The budget `residual` was tested against, as a fraction of the window. */ readonly tolerance: number | null; readonly witnessedAt: string | null; readonly witnessTokens: number | null; readonly peaksTokens: number | null; /** The harness's raw `used_percentage`, verbatim — see `usedPercentageRaw`. */ readonly witnessRawPercentage: number | null; /** Which reading produced `harnessPct`, so a unit misread is visible. */ readonly witnessPercentageUnit: PercentageUnit | null; } /** * Read the harness's context block out of a parsed statusline stdin payload. * Returns `null` only when there was no payload at all (a render on a TTY, or * a manual `peaks statusline`) — in which case there is nothing to record and * nothing to say. A payload that arrived but carried no usable percentage is * recorded, not dropped. */ export declare function parseHarnessWitness(input: { readonly stdin: StatusLineStdin | null; readonly nowMs: number; }): HarnessContextWitness | null; export declare function harnessWitnessPath(projectRoot: string, sessionId: string): string; /** * Write this render's record. Returns whether a file was written. * * This is the ONLY side effect on the statusline render path. It is bounded * (one small file, inside peaks-loop's own session directory), it cannot * influence any decision peaks-loop makes (nothing except the diagnostic in * `peaks code context-now` reads it), and it never throws — a statusline that * fails to render because an observability file could not be written would be * a worse failure than a missing observation. A failed write is not swallowed: * the next probe reports `absent`, which is the visible symptom. * * No `mkdir`: the session directory is created by the session layer, and a * statusline render is the wrong place to be creating directories. Its absence * is one of the honest reasons the witness can be missing. * * Written via temp + rename, not in place. A reader runs in ANOTHER process * (the probe) and treats unreadable JSON as "no witness" — so a torn read * would not be an error, it would be a SILENT loss of the comparison, which is * the failure mode this whole slice exists to remove. The rename makes the * partial state unobservable. Same shape as `atomicWriteJson` in * statusline-settings-service.ts. * * The rename is not always available: on Windows it throws EPERM while another * process holds the target open, which is exactly the reader this guard is * written for. Dropping the sample then would be the silent loss the temp * rename was introduced to remove, so the write falls back to in-place. The * fallback gives up atomicity for that one write — the reader may see a * half-written file and call it "no witness" — which is a narrower failure * than never recording the sample at all, and the temp path stays the normal * one. * * A LOWER-INFORMATION RECORD DOES NOT REPLACE A HIGHER-INFORMATION ONE (repair * cycle 3). The render that carries no readable percentage is still recorded * when there is nothing better on disk — that is what tells "rendered, nothing * usable" apart from "never rendered" (§17-B) — but it does NOT overwrite a * record whose percentage IS readable. Overwriting silences a real comparison: * measured 2026-09-14, a witness giving a real 3.3% window gap reported * `disagree` with its sentence, and one render whose payload lacked * `context_window` turned the same comparison into `unverifiable` with the * sentence suppressed. A render that says less must not delete a sample that * says more. * * AND THE PARSE IS INSIDE A TRY (repair cycle 3). The module's contract at the * top of this comment — "it never throws" — was true only by inspection: the * `parseHarnessWitness` call sat above the only `try`, and a payload shape the * guards did not anticipate escaped the function and killed the render (see * `parseHarnessWitness` for the measured case). A nested `try` rather than a * wider one, because the outer catch says something different: it is the * temp+rename FALLBACK, and a parse failure has no JSON to fall back TO. * A payload this module cannot read is a missing observation, which is the * outcome the contract already prefers. */ export declare function writeHarnessWitness(input: { readonly projectRoot: string | null; readonly sessionId: string | null; readonly stdin: StatusLineStdin | null; readonly nowMs: number; }): boolean; /** * What a read found. * * This used to be `HarnessContextWitness | null`, which collapsed FOUR file * states — absent, unreadable, not JSON, not the shape this module writes — * into one `null`. The caller then had to guess the cause from the directory * alone and picked `not-rendered` whenever the directory existed, so a render * whose record was merely unreadable was reported as "the statusline has not * rendered here": the wrong-cause sentence §17-B was written to remove. The * three states are named here instead of re-derived by a second look at the * disk. Same shape, and deliberately the same words (`missing` / `valid` / * `invalid`), as `CompactLifecycleRead` in * `src/services/compact-statusline/compact-lifecycle-store.ts`, which reads a * sibling file in the same directory for the same purpose. */ export type HarnessWitnessRead = { readonly kind: 'missing'; } | { readonly kind: 'valid'; readonly witness: HarnessContextWitness; } | { readonly kind: 'invalid'; }; /** * Read the record for a session. Anything that exists but cannot be used as a * record reads as `invalid` — never as `missing`, which would erase the one * fact the file's existence carries: a render happened here. */ export declare function readHarnessWitness(input: { readonly projectRoot: string; readonly sessionId: string; }): HarnessWitnessRead; /** * The budget, in tokens: what a difference between the two ratios can be * explained by WITHOUT the two denominators being different, once the * sampling skew has been taken out (see `compareHarnessWitness`). * * rounding 0.005 x window — the harness's percentage is rounded * numerator 0.0017 x usedTokens — measured disagreement of the two sums * * Sample skew is deliberately NOT a term here. It is not a budget at all: it * is MEASURED per sample from the two token counts and SUBTRACTED from the * deviation, because under equal denominators the token difference and the * ratio difference are the same number — see `compareHarnessWitness`. */ export declare function witnessToleranceTokens(input: { readonly windowTokens: number; readonly usedTokens: number; }): number; /** * Compare peaks-loop's ratio with the harness's own percentage. * * Two identifiers must match before any comparison is allowed: * 1. the SESSION — a witness written by another harness session on the same * project is not evidence about this one. Lenient in the same direction as * the compact-event attribution: refused only when BOTH ids resolve and * differ, because a guard that turns a missing field into a permanent * "cannot tell" is the failure this whole slice exists to remove. * 2. the MOMENT — a witness is a snapshot, and the harness documents that its * percentage depends on when it was calculated. The token counts are what * says whether the two numbers came from the same API response. * * THE MOMENT IS REMOVED, NOT BUDGETED (repair cycle 1). If the two ratios * share a denominator W, the harness's count and peaks-loop's count differ by * exactly the sampling skew, so * * peaksRatio - harnessPct == (peaksTokens - witnessTokens) / W * * holds identically — it is not a tolerance to be granted, it is an equality * to be tested. Budgeting the skew instead (`tolerance += |skew|`) made the * budget grow by `x` while the deviation grew by `x(1+g)`, so a real window * difference `g` was cancelled for every skew large enough to absorb it: a * measured 3.3% denominator gap read `agree` for skew in [12,400, 22,100] * tokens, and gaps up to 5.3% never surfaced at all. Subtracting the skew from * the deviation and testing the remainder against a budget that contains only * rounding and numerator disagreement makes the test invariant to skew by * construction, and leaves `-g x harnessPct/(1+g)` — the window difference * itself — as the only thing the residual can be. * * AND THE RESIDUAL'S SIZE IS THE WITNESS'S, NOT PEAKS-LOOP'S (repair cycle 2). * `-g x harnessPct/(1+g)` carries the witness's ratio, so a witness captured * while the session was small cannot show a window difference that a bigger * witness would. The three answers therefore do not share one gate: a residual * past the budget is a `disagree` at any witness size, while `agree` needs the * sample to be sharp enough to support it (`sampleSupportsAgreement`). */ export declare function compareHarnessWitness(input: { readonly witness: HarnessContextWitness | null; readonly peaksRatio: number; readonly peaksTokens: number | null; readonly peaksWindowTokens: number | null; readonly outerSessionId: string | null; readonly absentCause?: WitnessAbsentCause; }): HarnessWitnessComparison; /** * One-way sentence for a disagreeing witness. Advising, never asking: an * auto-compact observation must never become an `AskUserQuestion` (see * `.peaks/memory/auto-compact-threshold-policy.md`). * * The sentence states the quantity that decided it (the residual, after the * measured skew was removed) and the raw harness value with the reading that * was taken from it. Both are there so a reader can tell a real denominator * difference from a unit misread without going back to the file. */ export declare function describeHarnessWitness(comparison: HarnessWitnessComparison): string | null; /** Convenience for the CLI: read + compare in one call. */ export declare function readAndCompareHarnessWitness(input: { readonly projectRoot: string; readonly sessionId: string; readonly peaksRatio: number; readonly peaksTokens: number | null; readonly peaksWindowTokens: number | null; readonly outerSessionId: string | null; }): HarnessWitnessComparison;