/** * Rolling session summary — the thing that makes rehydration good rather than * merely possible. * * A resume brief built from the last N turns alone tells a fresh session what * just happened; it cannot tell it what the session was *for*. So, opt-in, we * periodically fold the whole record down to a ≤500-word summary and cache it * at `//summary.md`. The brief * (core/resume-brief.ts) picks it up when it's there and degrades cleanly to * last-N-turns when it isn't — which is the default, since this is * setting-gated (`HarnessSettings.rollingSummary`). * * NEVER BLOCKS A TURN. Everything here is fire-and-forget off the ingest path: * `noteEvent` returns synchronously, the fold runs as a headless one-shot * `TaskManager` job (a separate process, a cheap model, `--max-turns 1` — the * bounded shape `HarnessAdapter.launchTask` exists for), and every failure * mode short-circuits to "no summary". A user's session must never be slower, * or fail, because a summary didn't happen. * * The task does not write the file itself. It is asked for the summary text * and nothing else; we write `summary.md` from its `resultText` when it * completes. Two reasons: the target lives under `/`, outside the * session's cwd — which codex's `workspace-write` sandbox forbids and * claude-code's `acceptEdits` shouldn't be asked to reach — and a task that * writes its own output can half-write it, where a task that returns text * either produces a whole summary or none. * * WHERE IT DOESN'T WORK: `launchTask` is optional on the adapter contract and * codex has no non-interactive mode wired for it, so a codex session simply * never produces a summary and its briefs are last-N-turns. That is a real * gap, stated rather than papered over; the brief is honest either way. */ import type { AnalyticsEvent, BackgroundTask, HarnessKind, SessionRecord } from "../shared/types.js"; /** Filename under `//`. */ export declare const ROLLING_SUMMARY_FILE = "summary.md"; /** Completed turns that must accumulate before a re-fold is worth its cost. */ export declare const ROLLING_SUMMARY_TURN_INTERVAL = 10; /** The contract stated in the prompt, and the clamp applied to what comes back. */ export declare const ROLLING_SUMMARY_MAX_WORDS = 500; /** * Model alias for the fold. A summary of text we already hold is the cheapest * possible LLM job — pinning a small model here keeps an opt-in background * feature from quietly costing what the user's own session does. */ export declare const ROLLING_SUMMARY_MODEL = "haiku"; /** Hard turn cap: one turn in, one summary out. */ export declare const ROLLING_SUMMARY_MAX_TURNS = 1; /** Macro id the task is registered under — also TaskManager's dedupe key, so * a session can never have two folds in flight at once. */ export declare const ROLLING_SUMMARY_MACRO_ID = "rolling-summary"; /** * Token budget for the material handed to the fold. Larger than a brief's * (this is throwaway input to a cheap model, not a system prompt competing * with the user's own context) but still bounded, so a marathon session can't * turn one fold into a huge request. */ export declare const ROLLING_SUMMARY_INPUT_MAX_TOKENS = 20000; /** Absolute path of a session's cached summary. */ export declare function rollingSummaryPath(generatedRoot: string, harnessSessionId: string): string; /** * The cached summary for a session, or null when there is none — never throws. * A brief that fails to find a summary is a brief without one, not an error. */ export declare function readRollingSummary(generatedRoot: string, harnessSessionId: string): Promise; /** Trim `text` to at most `maxWords` words, marking the cut. */ export declare function clampToWords(text: string, maxWords?: number): string; /** * The one-shot prompt handed to `launchTask`. The material is the record * rendered by {@link buildResumeBrief} at a larger budget — deliberately the * same renderer the brief uses, so the fold reads exactly the shape a future * brief will present, and there is one place where "what a record looks like * as text" is defined. * * `previous` is folded back in rather than discarded: the record itself is * capped at the input budget, so on a long session the earliest turns are * already gone from the material, and the last summary is the only surviving * account of them. */ export declare function buildRollingSummaryPrompt(record: SessionRecord, previous: string | null): string; /** What the summarizer needs from the outside world. All injectable, so the * unit tests drive it without a filesystem or a spawned process. */ export interface RollingSummarizerDeps { /** Root the per-session generated dirs live under. */ generatedRoot: string; /** The `HarnessSettings.rollingSummary` gate, re-read per fold so toggling * the setting takes effect without a restart. Never throws. */ enabled: () => Promise; /** Reads the reconstructed record for a session (SessionRecordReader.read). */ readRecord: (harnessSessionId: string) => Promise; /** The live session, for the harness kind and cwd the task runs as. Returns * undefined once the session is gone from the registry. */ getSession: (harnessSessionId: string) => { harness: HarnessKind; cwd: string; } | undefined; /** TaskManager.run, narrowed to what this needs. */ runTask: (req: { macroId: string; label: string; harnessSessionId: string; harness: HarnessKind; cwd: string; prompt: string; model: string; maxTurns: number; }) => Promise; /** Completed turns between folds. Default {@link ROLLING_SUMMARY_TURN_INTERVAL}. */ turnInterval?: number; /** Diagnostics sink. Defaults to console.error — a fold failing is worth a * log line and nothing more. */ onError?: (err: unknown) => void; } export interface RollingSummarizer { /** * Feed one normalized analytics event. Synchronous and total: it counts * `turn.completed`, triggers a fold on the interval and on `session.end`, * and ignores everything else. Any work it starts is detached. */ noteEvent(event: AnalyticsEvent): void; /** * Feed a task status change (TaskManager.onStatusChange). A completed fold * task's `resultText` is what becomes `summary.md`. */ noteTaskStatus(task: BackgroundTask): void; /** Resolves when no fold this summarizer started is still in flight. For * tests and for shutdown; production callers never need to await it. */ idle(): Promise; } export declare function createRollingSummarizer(deps: RollingSummarizerDeps): RollingSummarizer; //# sourceMappingURL=rolling-summary.d.ts.map