import type { WardenMode } from "./config.js"; import type { Level, PreviousAction, ScopeLabel, Verdict } from "./guard.js"; /** * Hold feedback loop. Every hold is a prediction ("this call should not run as it stands") and every allowed call the * opposite one; what the user does next labels them, so hold precision can be measured on real sessions instead of the * synthetic cases the thresholds came from: * * - **approved**: the user's reply released the hold (steer mode) or the confirm dialog allowed it. False positive. * - **declined**: the confirm dialog refused it. True positive. * - **replanned**: the user replied, the following turn ended, and nobody approved the call. True positive. * - **regretted**: the user's next message tells the agent to stop, undo, or not do an allowed call. False negative. * - **accepted**: the user's next message was checked and does not regret the allowed calls of the last turn. * * Approval, decline, and re-plan are code only. Regret is one question that rides the first action request under the * new prompt (no extra request); offline a stop-word heuristic stands in. Calls the guard skipped as read-only are not * recorded: they could never have been held. Records are redacted for the log: tool, pattern ids, scores, level, * outcome, never the command or path. The in-memory summary serves the regret question and stays in memory. */ export type CallOutcome = "pending" | "approved" | "declined" | "replanned" | "regretted" | "accepted"; export type OutcomeVia = "retry" | "dialog" | "next prompt" | "jev" | "text" | "deny"; export interface CallScores { irreversible: number; offTask: number; scope: ScopeLabel; mutates?: number; approved?: number; intentMismatch?: number; visible?: number; securityRisk?: number; } export interface CallRecord { /** Sequence number within the session; the regret question names candidates by it. */ id: number; at: number; tool: string; level: Level; source: Verdict["source"]; mode: WardenMode; held: boolean; patterns: string[]; /** Reason labels as shown to the user; they name patterns and scores, never the command. */ reasons: string[]; /** Length of the agent's stated plan sent with the call; 0 means the agent said nothing before calling. Decides whether declared intent (layer 2) is worth building. */ planChars: number; scores?: CallScores; outcome: CallOutcome; outcomeAt?: number; outcomeVia?: OutcomeVia; /** P(regret) from the question that labelled this call, when Jev answered it. */ regret?: number; } export interface HoldSnapshot { holds: number; approved: number; declined: number; replanned: number; /** Holds without a label yet: the user has not replied, or the turn after the reply is still running. */ awaiting: number; allowed: number; regretted: number; accepted: number; /** Labelled holds: approved + declined + replanned. */ labels: number; /** (declined + replanned) / labels; undefined without labels. */ precision: number | undefined; } export declare class HoldLedger { private readonly tracked; private readonly untracked; private nextId; /** One inspected call. `outcome` is set at once when the confirm dialog decided; a steer-mode hold starts pending. */ record(verdict: Verdict, options: { held: boolean; mode: WardenMode; outcome?: CallOutcome | undefined; via?: OutcomeVia | undefined; at?: number; task?: string; plan?: string; contextSummary?: string; agentReason?: string; }): CallRecord; /** * The user's reply released a hold: the pending hold of the same tool is the false positive, the latest one when the * tools differ (the retry rarely repeats the held call byte for byte, and a hold can be released through another tool). */ approved(tool: string, via?: OutcomeVia, at?: number): CallRecord | undefined; /** * A new user prompt. Every pending record has seen one more prompt. A hold still pending after the prompt that could * have approved it (the reply) and the turn that followed is a re-plan: the agent found another way or the user said no. */ promptArrived(at?: number): CallRecord[]; /** Allowed calls of the turn the user just replied to, oldest first: the calls the reply could regret. */ candidates(): PreviousAction[]; /** * The reply was read. With regret, the located call (or the latest candidate) is the miss and the rest are accepted; * without it, every candidate is accepted. Returns the records that changed. */ regret(result: { regretted: boolean; target?: string | undefined; probability?: number | undefined; via: OutcomeVia; }, at?: number): CallRecord[]; records(): readonly CallRecord[]; /** * Observability for the rules guard: its verdicts never act, so they do not join the regret labelling, but they * land in the hold log (tool "rules") with the per-rule violation probabilities so eval runs can diagnose misses. */ recordRules(input: { source: Verdict["source"]; path?: string; findings: readonly { name: string; violation: number; }[]; error?: string; }): CallRecord; snapshot(): HoldSnapshot; reset(): void; private label; } /** Whether P(regret) from the question counts as regret. */ export declare function regretsAt(probability: number): boolean; /** Offline stand-in for the regret question: the reply opens by stopping, undoing, or forbidding what was just done. */ export declare function textRegrets(prompt: string | undefined): boolean; /** One line for the trace entry of a call whose outcome landed. */ export declare function outcomeNote(record: CallRecord): string; /** One line for /warden status. */ export declare function formatHolds(snapshot: HoldSnapshot, logPath?: string): string; /** Per-session file beside the user config: `/pi-warden/holds/-.jsonl`. */ export declare function holdLogPath(sessionId: string, at?: Date): string; /** Rewrites the session's records as JSON lines, owner-only, one write at a time so outcomes never interleave. */ export declare class HoldLog { readonly path: string; private queue; private failure; constructor(path: string); save(records: readonly CallRecord[]): Promise; /** The last write error, if the most recent save failed. */ get lastFailure(): string | undefined; }