/** * Harness auto-compact window ownership * * The goal (user's words): *when* to trigger auto-compact must be * peaks-loop's decision; *how* to compact stays the harness's capability. * * That only holds if the two sides agree on ONE number. Before this slice * they each resolved a window independently: Claude Code used the window it * knows for the model, while peaks-loop divided by `resolveContextWindow()` * (env → config → model-name heuristic → 200_000 default). A 1M-window model * whose id the heuristic does not recognise made peaks-loop's "95%" land at * 190K while the harness waited for ~967K — a 5× early trigger, and (before * T3) a deadlock, because nothing could lower the ratio. * * This module owns the OTHER half of the fix: the harness itself accepts a * window override (`IdeCompactProfile.autoCompactWindowEnvVar`, declared per * adapter — no IDE names here), so peaks-loop writes the very number it * computes the ratio against into the harness's own machine-local settings * file. Reader and writer then reference one artifact instead of two * resolutions that must "remember" to agree. * * Vendor neutrality: this module knows about a JSON file with an `env` block * and nothing else. The settings path and the env-var name are BOTH passed * in by the caller, resolved from the IDE adapter's declarations. There is * no IDE id, no `.claude` literal, and no registry import in this file. */ /** * Peaks-owned opt-out flag, read from the SAME machine-local `env` block the * window key is written to. * * Why it exists: `resetHarnessWindow` must be a real rollback. Without a * durable opt-out, the next context probe would simply write the key back * and the user's "remove it" would last until the next tool call — a * rollback in name only. Absent (or any value other than `off`) = sync is * enabled, which keeps every project installed by an earlier release * unchanged. */ export declare const HARNESS_WINDOW_SYNC_OPTOUT_KEY = "PEAKS_HARNESS_WINDOW_SYNC"; /** The one value that disables the sync. */ export declare const HARNESS_WINDOW_SYNC_OPTOUT_VALUE = "off"; /** * Provenance marker: the window value peaks-loop ITSELF last wrote, recorded * in the same `env` block as the window key. * * Why it must exist. The window key is a SHARED artifact — the harness reads * it, the user may set it by hand (it is a documented Claude Code variable), * and peaks-loop writes it. Two rules collide on it: * * - the ratio peaks-loop reports must divide by the key (the single-source * rule), so the key outranks the model heuristic; and * - a first-time mis-resolution must not become PERMANENT. * * The second rule needs the late 1M rescue to be able to overrule the key when * the observed context proves the key too small. Applied to a value a HUMAN * pinned, that same rescue would silently destroy an explicit setting — the * user asks for an early compact, a long session outgrows the pin, and * peaks-loop rewrites the file to 1M, permanently. Applied to peaks-loop's own * earlier output it is simply self-correction. * * This marker is what tells the two apart: it records what peaks-loop wrote, * so a mismatch means the human has taken the key over and the rescue must * stand down. It travels in the same write as the value it describes, so the * two can never disagree about which write they belong to. */ export declare const HARNESS_WINDOW_WRITTEN_KEY = "PEAKS_HARNESS_WINDOW_WRITTEN"; /** * The harness's OWN accepted band for its auto-compact window (E1, rid * * THE BAND IS A PROPERTY OF THE KEY, NOT OF peaks-loop. `autoCompactWindow` — * and the `CLAUDE_CODE_AUTO_COMPACT_WINDOW` env var that shadows it — is * documented as "how full the context window gets before Claude Code compacts * automatically, in tokens from 100000 to 1000000", and a value outside that * band is not the window the harness compacts on: below the minimum the value * is ignored, above the maximum it is reduced to the model's own context size * (`Math.min(native, override)`; no Claude model's window exceeds 1000000, so * above the maximum the reduction is certain). * * Either way the outcome is the failure this whole module exists to prevent: * peaks-loop divides its ratio by a number the harness is not using, so * "85%" names a point the harness will never fire at. A positive integer is * therefore NOT sufficient — the band is part of what the value means. * * Clamping instead was rejected. A clamped write would put a DIFFERENT number * in the file from the one the probe just divided by, restoring the * two-resolutions drift inside the single write that is supposed to delete it. * Refusing keeps the disagreement in one place, reportable, and reported. */ export declare const HARNESS_WINDOW_MIN_TOKENS = 100000; /** See `HARNESS_WINDOW_MIN_TOKENS`. */ export declare const HARNESS_WINDOW_MAX_TOKENS = 1000000; /** * True when `tokens` is a window the harness will actually compact on. * `null` (absent / unparseable) is NOT in band — there is no window at all. */ export declare function isHarnessWindowInRange(tokens: number | null): boolean; /** Where the harness key lives + which key it is. Both caller-supplied. */ export interface HarnessWindowLocation { /** Absolute path of the machine-local settings file. */ readonly settingsPath: string; /** Env var / settings key the harness reads its auto-compact window from. */ readonly envVar: string; /** * The project root `settingsPath` was derived from, when the caller knows it. * * Why the writer wants it: `--project .` with the shell sitting in the user's * home directory resolves the "project root" to `$HOME` itself, which puts * this file at `$HOME/.claude/settings.local.json` — the user's PERSONAL * harness settings, shared by every project they own and outside any repo * peaks-loop has business editing. A new terminal starts in `$HOME`, so that * combination is the ordinary one, not an exotic one. The writer refuses it * (reason `unsafe-project-root`) and says so instead of writing. * * Optional: callers that legitimately operate on a temp directory or on a * path they built themselves simply omit it, and no check is made. The * comparison lives in `isUserHomeProjectRoot` (`config-safety.ts`) and is * never string equality — a `--project` argument may arrive as `C:/Users/x` * where `homedir()` yields `C:\Users\x`. */ readonly projectRoot?: string; } /** Which artifact a value was read from. */ export type HarnessWindowValueSource = 'process-env' | 'settings-file'; export interface HarnessWindowReadResult { /** Parsed window in tokens, or null when absent / not a positive integer. */ readonly tokens: number | null; /** The raw on-disk / in-env value, for diagnostics. */ readonly raw: unknown; /** Where the value came from; null when neither side carries it. */ readonly source: HarnessWindowValueSource | null; /** * The SETTINGS FILE's window, parsed — the value the harness will carry * tomorrow, independent of the frozen process env. `null` when the file is * silent or its value is unparseable. * * This is the T1 number, and it is deliberately separate from `tokens`: * `tokens` answers "what is in force in the running session?" (env-first, * for the status read), while `fileTokens` answers "what is configured for * the harness?" — the one artifact peaks-loop can keep consistent with its * own ratio. See `resolveHarnessRatioWindow` in `auto-compact-reader.ts`. */ readonly fileTokens: number | null; /** * The settings file's raw value, `undefined` when the file carries no such * key. Presence-detection only; use `fileTokens` for the number. * * Needed because "the file says 150000" and "the file says something * unparseable" must both be distinguishable from "the file is silent": the * first two are values peaks-loop must not overwrite, the third is an empty * slot it may fill. */ readonly fileRaw: unknown; /** True when `settings.local.json` opts out of peaks-loop managing it. */ readonly optedOut: boolean; /** * True when peaks-loop is the current writer of this key — the file's window * value still matches the `HARNESS_WINDOW_WRITTEN_KEY` marker peaks-loop * wrote with it. False when the key is unset, or when its value differs from * the marker, i.e. a human has taken the key over. * * Deliberately decided from the FILE's value, not from whichever copy * (`process-env` / `settings-file`) supplied `tokens` this time. A running * session freezes its env at start-up, so after peaks-loop raises the file * the env still hands back the superseded number; judging by that copy would * report "a human set this" about peaks-loop's own value and switch the late * 1M rescue off on the next probe. * * The caller uses this to decide whether the late 1M rescue may overrule the * harness layer: peaks-loop's own output may be corrected, a human's pin may * not. See `resolveContextWindowTokens`. */ readonly peakWritten: boolean; } export type HarnessWindowSyncAction = 'written' | 'unchanged' | 'skipped'; export interface HarnessWindowSyncResult { readonly settingsPath: string; /** The key written / read (the adapter-declared `autoCompactWindowEnvVar`). */ readonly key: string; readonly action: HarnessWindowSyncAction; /** * Why the sync did nothing. Present only when `action === 'skipped'`: * - `no-window-resolved` — the caller had no token window to write * (percent-only probe sources carry none) * - `opted-out` — the user ran the rollback * - `unreadable-settings` — the file exists but is not a JSON object we * can safely edit * - `not-peaks-owned` — the file already carries a value that * peaks-loop did not write (a human's pin, or * another tool's). peaks-loop never rewrites it * and never re-arms its provenance marker for * it; the value wins over peaks-loop's own * resolution instead. This is the B1 guard — * without it a stale process env (or any * re-resolution) silently reverts a human's * hand-edited window and re-claims ownership. * - `unsafe-project-root` — the resolved project root IS the user's home * directory, so this write would land in their * personal `~/.claude/settings.local.json` * (`--project .` from a fresh terminal). The H1 * guard; `--reset` is deliberately still allowed * there, because removing a key an earlier * release put in `$HOME` is the user's explicit * recovery path. */ readonly reason?: string; /** The value in force after the call. */ readonly tokens: number | null; /** * The value the SETTINGS FILE held before the call (null when absent — or * when the file is unreadable). Deliberately the file's value, not the * process env's: the file is what the next session reads, so it is what the * caller must quote back to the user ("was 200000") and what decides whether * a write is needed at all. */ readonly previousTokens: number | null; /** * The window the caller ASKED to materialize — the denominator it computed * that probe's ratio against (`probe.capacityTokens`). `null` when the * caller carried no token window at all. * * Why it is reported rather than left at the call site: the interesting * outcome is a DISAGREEMENT, and a disagreement takes two numbers. A result * that carried only "what is in force" left every consumer unable to name * what it disagrees WITH — the notice for a refused write could say that * nothing was written and never which two numbers were apart. See * `describeHarnessWindowSync`. */ readonly requestedTokens: number | null; /** * The file's RAW value for the key before the call; `undefined` when the * file carried no such key. * * The one case `previousTokens` cannot express: a hand-typed `500k` parses * to `null`, exactly like an absent key, yet the two need opposite notices — * an absent key is a slot peaks-loop may fill, a typo is a value it must not * touch. Only the raw text can be quoted back to the user, so it travels * with the result. */ readonly previousRawValue: unknown; /** * True when the window in the file after the call is peaks-loop's own write * (its value matches the provenance marker). * * Reported because "the file already holds the number we wanted" has two very * different meanings and only this flag tells them apart: peaks-loop's own * earlier output, which it may raise when a session outgrows it, versus a * value a human pinned, which it never will. Without it, `unchanged` reads as * "all good" for a key peaks-loop has permanently stopped managing — the * state a user upgrading from a pre-marker release is silently in. * * Always `true` for `action: 'written'`: the write that produced the result * is what sets the marker. */ readonly peakWritten: boolean; } export type HarnessWindowResetResult = { readonly settingsPath: string; readonly action: 'removed' | 'absent'; readonly previousTokens: number | null; }; /** * The ONE wording of "what the harness-window sync just did", for both * commands that sync (`peaks code context-now`, `peaks code auto-compact`). * * Why it lives here rather than inline at each call site: peaks-loop rewrites * a file in the user's own harness settings on every probe, and the user * accepted that write on one condition — 要告知 (tell me). Two copies of the * sentence would mean a future edit tells half the users. This is the only * definition; the CLI renders whatever it returns. * * The `written` branch is the load-bearing one: it names the key, the value, * the file, what the value was before, and the exact rollback command, because * the harness itself reports an override only through `/autocompact` — if * peaks-loop does not say it here, nobody says it. * * The `skipped` branch is the one that is easy to get wrong, and did: it used * to end at the reason token (`Harness window not managed (not-peaks-owned).`), * which is true and tells the reader nothing. A refusal can be the exact * moment the two sides came apart — the ratio divided by one number while the * file pins another — so it is composed from the reason, the two numbers when * they disagree, and the raw value when the file's value is not a number at * all. See `harnessWindowConflictClause` / `unreadableWindowValueClause`. */ export declare function describeHarnessWindowSync(result: HarnessWindowSyncResult | null): string; /** * The same disagreement as `harnessWindowConflictClause`, reduced to one line * for the `warnings` channel — the machine-readable half of 要告知, so a * consumer reading the JSON envelope (or a human reading stderr) sees it even * if it never renders `nextActions`. Returns `null` when there is nothing to * warn about, which is the ordinary case. */ export declare function harnessWindowSyncWarning(result: HarnessWindowSyncResult | null): string | null; /** * Parse a candidate window value. Positive finite integers only — a number, * or a numeric string (the env block is JSON, so the value on disk is always * a string there). The harness's own documentation is explicit that the * variable "accepts only the plain token count" and NOT a `850k` suffix, so * anything non-integral is refused here rather than written and silently * ignored downstream. */ export declare function parseHarnessWindowTokens(raw: unknown): number | null; /** * Read the window the harness is currently using for auto-compact. * * Order: the process env FIRST (that is the value the RUNNING session * captured at start-up, so it is what the harness is actually compacting * against right now), then the settings file (what the next session will * read). When the two disagree, the file is what the sync updates — the * process env cannot be changed from inside a running session. */ export declare function readHarnessWindow(input: { readonly location: HarnessWindowLocation; readonly env?: NodeJS.ProcessEnv | undefined; }): HarnessWindowReadResult; /** * Write `tokens` as the harness's auto-compact window. Idempotent: a re-run * with the same value performs no write at all, so repeated calls (the sync * runs on every context probe) cannot churn the file or duplicate the entry. * * Everything else in the file is preserved verbatim — the caller's own `env` * entries, every `hooks` entry, and any unknown top-level key. This is the * same read-modify-write discipline `auto-compact-hook-install.ts` already * applies to this file; there are now two writers, and both must leave the * other's rows alone. */ export declare function syncHarnessWindow(input: { readonly location: HarnessWindowLocation; readonly tokens: number | null; readonly env?: NodeJS.ProcessEnv | undefined; }): HarnessWindowSyncResult; /** * Rollback: remove the window key and record the opt-out so the next sync * does not put it straight back. Both edits land in the same * read-modify-write, so there is no window in which the key is gone but the * opt-out is not yet on disk. * * Idempotent: a second call reports `action: 'absent'` and rewrites nothing * unless the opt-out row is missing. */ export declare function resetHarnessWindow(input: { readonly location: HarnessWindowLocation; readonly env?: NodeJS.ProcessEnv | undefined; }): HarnessWindowResetResult; /** * Record the opt-out WITHOUT removing anything: "stop managing this key", * expressible at any moment — including before peaks-loop has ever written it. * * WHY THIS IS A SEPARATE ENTRY POINT AND NOT A FALLBACK OF `resetHarnessWindow` * * "Remove what is there" and "never write it again" are two different * intentions. `--reset` on a file holding no peaks row is deliberately a no-op * (see the file-litter note in `resetHarnessWindow`), so a user whose project * peaks-loop has never touched had NO command for the second intention at all: * they had to wait for a probe to write the key and then remove it — the order * backwards. Folding the opt-out into `--reset` would fix that by making one * verb mean "delete" or "don't write", depending on whether the file happened * to hold a row, which is a verb whose effect the user cannot predict from its * name. Two verbs, two intentions, each predictable. * * WHAT IT DOES NOT DO * - it does not touch the window key: a hand-set value stays exactly as it * was (peaks-loop would not have overwritten it anyway — see the B1 guard); * - it does not write a provenance marker, so it claims no ownership of a * value it did not write. * The next probe reports `skipped / opted-out` and writes nothing. * * * This function used to exempt itself, on the argument that `--disable` is an * explicit instruction and "the location's own file is the only place the * opt-out can be recorded for it to mean anything". The first half is true and * was never the problem; the second half does not hold at `$HOME`: * * - the location is NOT usually explicit. `--project` is optional, and a * fresh terminal starts in `$HOME`, so the ordinary invocation resolves the * root to the user's home directory without them naming it — the very * trigger the H1 guard was written for; * - the opt-out has NOTHING to mean there. `syncHarnessWindow` already * refuses to write the window at that root (`unsafe-project-root`), so the * only thing a recorded opt-out changes is which sentence the refusal uses. * Nothing is made expressible; a visible refusal is traded for a quieter one; * - what IS added is a durable peaks-loop row in `~/.claude/settings.local.json` * — a file outside every repo. `--reset`'s exemption does not transfer to * this verb: `--reset` REMOVES (and is a no-op when there is nothing of * peaks-loop's to remove), while `--disable` only ever ADDS. * * So the guard fires here exactly as it does in `syncHarnessWindow`: the same * exact-home comparison, before any `mkdir` and before any read-modify-write. * `~/my-project` is unaffected, and a user who really does carry a stale * peaks-loop window key in `$HOME` still has `--reset`, which is allowed there * for the reason its own note gives. * * Idempotent: a second call reports `already-opted-out` and rewrites nothing. */ export declare function disableHarnessWindowSync(input: { readonly location: HarnessWindowLocation; }): { readonly settingsPath: string; readonly action: 'disabled' | 'already-opted-out' | 'unreadable-settings' | 'refused-unsafe-project-root'; }; /** * Undo the opt-out (the companion of `resetHarnessWindow`) so peaks-loop * resumes owning the harness window. */ export declare function reenableHarnessWindowSync(input: { readonly location: HarnessWindowLocation; }): { readonly settingsPath: string; readonly action: 'reenabled' | 'absent'; };