/** * Session structural ledger (change: add-session-structural-ledger). * * One small record per MCP server process — the session, as for the epistemic lease — of the * symbols the agent was SERVED and the symbols that were EDITED, each with its normalized content * hash (`symbol-content-hash.ts`: whitespace and comments do not change it, a real edit does). It * stores addresses and hashes only, never file contents. * * Two facts come from it, both stated neutrally and never blocking: * * - **Background change.** A served symbol whose body changed since it was served, where the change * is not the agent's own. "Own" is decided by the harness's after-edit hook (`openlore check-edit * --hook`, or the agent-loop hooks), which appends each file the agent edits to the worktree's * agent-edit log. Without any such report in this session the note is withheld, with the reason in * `openlore status`, because an edit cannot otherwise be told apart from another process's. * - **Edit oscillation.** A symbol that returns, for the second time, to a version it already had * this session (A → B → A → B). The versions come from the MCP watcher's re-index batches when the * watcher runs in this process, and are otherwise sampled at each tool call (disclosed). * * Other processes (status, the panic-check hook) read the persisted ledger of the single live * server for the worktree, and withhold the facts when there is none or more than one. */ /** Where ledgers live, under `.openlore/`. */ export declare const SESSION_LEDGER_DIR = "session-ledger"; /** The worktree's agent-edit log, appended by the after-edit hook. */ export declare const AGENT_EDITS_FILE = "agent-edits.jsonl"; /** A ledger not written for this long belongs to no live session (the panic-state session rule). */ export declare const LEDGER_EXPIRY_MS: number; /** Returns to an earlier version before a loop is reported (A → B → A → B: two). */ export declare const OSCILLATION_RETURNS = 2; export interface ServedEntry { id: string; name: string; file: string; hash: string; servedAt: number; kind?: 'contract' | 'iac'; } /** One version of a symbol: its hash, when it was observed, and whether by sampling at a tool call. */ /** A symbol's version: its body hash, and its declaration-line hash when the parser gave one. */ interface Version { hash: string; at: number; sampled: boolean; sig?: string; } export interface EditedEntry { id: string; name: string; file: string; versions: Version[]; returns: number; exported?: boolean; kind?: 'contract' | 'iac'; } /** * How the agent's edits changed the session's symbols (change: add-ledger-contract-hashes-and-counts): * counts only, never a percentage or threshold. A code symbol is counted when the agent's after-edit * hook attributes one of its versions to the agent and its latest version differs from its first: * in exactly one of `bodyOnly`, `signatureChanged`, `unclassified` (a declaration line missing or one * the parser may have cut), or `firstSeenAfterEdit` (its first observed version was already the * agent's edit, so its baseline is unknown). `exportsChanged` counts the changed ones a module exports. * A DECLARATION (an OpenAPI operation, an infrastructure resource) has no declaration line to * compare, so it is counted on its own line instead — `contractElementsChanged` or * `iacResourcesChanged` — and never as `unclassified`. */ export interface ChangeKinds { bodyOnly: number; signatureChanged: number; unclassified: number; firstSeenAfterEdit: number; exportsChanged: number; /** Declared OpenAPI operations the agent changed (change: add-ledger-contract-hashes). */ contractElementsChanged: number; /** Declared infrastructure resources the agent changed. */ iacResourcesChanged: number; } export interface BackgroundChange { id: string; name: string; file: string; servedAt: string; changedAt: string; } export interface EditOscillation { id: string; name: string; file: string; returns: number; cycle: string[]; } /** The persisted ledger, as other processes read it. */ export interface LedgerSnapshot { schemaVersion: 1; pid: number; root: string; startedAt: string; updatedAt: string; /** How edit versions are observed: the in-process watcher, or sampling at tool calls. */ editSource: 'watcher' | 'sampled'; /** True once an after-edit hook reported an agent edit this session. */ agentEditsReported: boolean; counts: { served: number; edited: number; untracked: number; dropped: number; }; changeKinds?: ChangeKinds; /** Watcher batches of more than 20 files (a branch switch, a stash), which record no versions. */ bulkBatches?: number; /** * Symbols this session saw change (more than one version), first {@link MAX_EDITED_LISTED}: the * work in flight, even once committed (the handoff briefing's touched set). */ edited?: Array<{ id: string; name: string; file: string; kind?: 'contract' | 'iac'; }>; backgroundChanges: BackgroundChange[]; editOscillation: EditOscillation[]; /** Why background-change notes are withheld, when they are. */ withheld?: string; } /** * One tracked element of a file: its normalized hash, and what KIND of thing it is. A declaration * (an OpenAPI operation, an infrastructure resource) is tracked exactly like a code symbol — same * ids, same versions, same notes — but counted on its own line (change: add-ledger-contract-hashes). */ export interface HashedElement { name: string; hash: string; sig?: string; exported?: boolean; kind?: 'contract' | 'iac'; } /** Normalized per-symbol hashes for one file's text; `undefined` when the file cannot be hashed. */ export type FileHasher = (file: string, content: string) => Promise | undefined>; /** The default hasher: the same extraction and hash the index and changed-set use. */ export declare const hashFileSymbols: FileHasher; /** A whole-file hash for non-code files a briefing names (spec Markdown): whitespace-normalized text. */ export declare const hashWholeFile: FileHasher; /** * A declaration file is hashed BOTH ways: one entry per element it declares, AND one whole-file * entry beside them. * * The per-element entries are the point of this — a note that names `POST /v1/checkout` rather * than `openapi.yaml`. The whole-file entry is the backstop, and it is not optional: an element * model is always narrower than the file. A path added, an operation deleted, a `security` * requirement, a non-2xx response, an infrastructure property the IaC model does not keep, a * specification that stops parsing mid-session — each is a real change to something the agent * read, and each would otherwise be seen by nothing at all. Keeping both means the worst case is * a change named by its file, which is what this ledger did before elements existed, and never * silence (change: add-ledger-contract-hashes). * * Otherwise: code symbols, or — for a file a briefing can name whole — one whole-file hash. */ export declare const defaultHasher: FileHasher; /** Create the ledger directory with its own `.gitignore`: session files are never committed. */ export declare function ensureLedgerDir(root: string): string; /** Append one agent edit to the worktree's log (the after-edit hook's only write). Never throws. */ export declare function recordAgentEdit(root: string, file: string, at?: number): void; /** The in-process ledger of one MCP server session. */ export declare class SessionLedger { private readonly hasher; private readonly now; readonly root: string; readonly startedAt: number; private served; /** Each tracked symbol's first observed version, for the session's net change. */ private firstVersion; private edited; /** Last seen mtime of each tracked file, so unchanged files are not re-hashed. */ private seenMtime; private untracked; /** Watcher batches too large to be edits, recorded as no versions. */ private bulkBatches; /** Names being hashed by resolveServed, so a serve arriving meanwhile keeps them. */ private resolving; private dropped; /** Facts already stated in a note: `id@hash` for background changes, `id#returns` for loops. */ private delivered; /** Loops already handed to the panic layer. */ private loopsSeen; private editSource; private writeTimer; /** Serializes the watcher and sampling work so versions land in the order they were observed. */ private queue; /** Reading of served files: its own chain, which a tool call never waits for. */ private servedQueue; /** * Served files not hashed yet: the text the agent was shown, held in memory (never persisted) and * hashed only once the file changes — so serving costs a read, not a parse. */ private pending; private pendingBytes; private disposed; constructor(root: string, hasher?: FileHasher, now?: () => number); /** The watcher runs in this process: edit versions come from its batches. */ setWatcherActive(active: boolean): void; private enqueue; /** Wait for all queued hashing (tests). */ settle(): Promise; private readCurrent; private cap; /** * Record what a tool response served: symbols by (name, file), or whole files (every symbol in * them, for a file-level answer such as a skeleton). The text is kept until the file changes and * hashed then; a file too large to keep is counted untracked. */ recordServed(items: ReadonlyArray<{ file: string; name?: string; }>): Promise; /** Hash a served file's kept text: the versions the agent read. Names the hasher lacks are untracked. */ private resolveServed; /** * Re-indexed files from the in-process watcher: each symbol's new version, observed `at` the * batch. A bulk batch hashes the files this session tracks first and counts the rest untracked. */ recordReindexed(files: ReadonlyArray<{ rel: string; content: string; }>, at?: number): Promise; private mtime; private observe; /** Append a symbol's version; consecutive equal versions are one version. */ private noteVersion; /** * At a tool call: sample the tracked files that changed on disk (the served ones always; all of * them when no watcher feeds versions). Bounded: a tool call waits at most {@link REFRESH_BUDGET_MS} * for queued work, then answers with what is known. */ refresh(budgetMs?: number): Promise; /** * Whether a version is the agent's own edit: the after-edit hook reported its file around when the * watcher observed it, or, for a version sampled at a tool call, at any time since the version * before it was observed (sampling cannot separate edits that landed between two calls). */ private ownVersion; /** The current facts. `agentEdits` defaults to the worktree's agent-edit log since the session began. */ facts(agentEdits?: { file: string; at: number; }[]): { backgroundChanges: BackgroundChange[]; editOscillation: EditOscillation[]; agentEditsReported: boolean; withheld?: string; }; /** * The neutral note for the next tool response: new background changes (at most three named, the * rest counted) and new edit loops. Each fact is stated once. `null` when there is nothing new. * Names and paths are screened repository text. */ takeNote(agentEdits?: Array<{ file: string; at: number; }>): string | null; /** * Symbols that reached an edit loop since the last call (the panic layer's input): each symbol * once, when it first returns to an earlier version for the second time. Further returns are in * the note and the status, but add no more to the panic score. */ takeNewOscillations(): EditOscillation[]; /** * How the agent's edits changed the session's code symbols, net of first and latest versions. * Undefined without an after-edit report: the agent's edits cannot be told from others'. */ changeKinds(agentEdits?: { file: string; at: number; }[]): ChangeKinds | undefined; /** Whether the version was the one served (its text was read through OpenLore before any edit). */ private servedBefore; snapshot(): LedgerSnapshot; private scheduleWrite; /** Write the snapshot for other processes (atomic; failures are ignored). */ persist(): Promise; /** Remove this session's ledger (process exit). */ dispose(): void; } /** Collect (file, name) pairs a tool result served. `name` absent means the whole file. */ export declare function servedItems(tool: string, result: unknown): Array<{ file: string; name?: string; }>; /** * The live session's ledger for a worktree, as a separate process (status, a hook) reads it: exactly * one live server's snapshot, or the reason none is used. Never guesses between several. */ export declare function readLiveLedger(root: string, now?: number): { ledger: LedgerSnapshot; } | { withheld: string; }; export {}; //# sourceMappingURL=session-ledger.d.ts.map