pi-warden 0.60.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
- import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
+ import { writeFileAtomicSync } from "./atomic.js";
4
5
  import { resolveBackend } from "./backend.js";
5
6
  import { COMMAND_TOOLS } from "./tools.js";
6
7
  import { defaultWidgetConfig } from "./widget.js";
@@ -23,7 +24,7 @@ export function isRecallTool(value) {
23
24
  }
24
25
  export const PACKAGE_NAME = "pi-warden";
25
26
  /** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
26
- export const CONFIG_SCHEMA = 8;
27
+ export const CONFIG_SCHEMA = 10;
27
28
  export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
28
29
  export function defaultConfig() {
29
30
  return {
@@ -42,6 +43,7 @@ export function defaultConfig() {
42
43
  offTask: { warn: 0.6, steer: 0.85 },
43
44
  intentMismatch: 0.9,
44
45
  visibleMismatch: 0.8,
46
+ intentTraceOnly: "invisible",
45
47
  shouldProceed: { hold: 0.6, steer: false },
46
48
  feedbackLog: true,
47
49
  commandRules: [],
@@ -52,12 +54,12 @@ export function defaultConfig() {
52
54
  escalationThreshold: 0.85,
53
55
  floor: "evidence",
54
56
  },
55
- stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, churnThreshold: 5, nudge: true, repeatSteer: true, diffLimit: 3000, tailLimit: 1000 },
57
+ stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, churnThreshold: 5, nudge: true, repeatSteer: true, evidence: true, diffLimit: 3000, tailLimit: 1000 },
56
58
  done: { enabled: true, claimsDone: 0.7, nudge: true, uiProof: true, uiFiles: [...DEFAULT_UI_FILES], visualTools: defaultVisualTools() },
57
59
  slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
58
60
  security: { enabled: true, threshold: 0.7, maskOutput: true },
59
- rules: { enabled: true, threshold: 0.7, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
60
- context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true, largeOutput: { enabled: true, threshold: 0.85 } },
61
+ rules: { enabled: true, threshold: 0.7, softThreshold: 0, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
62
+ context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true, dedupeRuns: true, dedupeMessages: false, largeOutput: { enabled: true, threshold: 0.85 } },
61
63
  runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
62
64
  notify: { enabled: false, cooldownMs: 10000, command: [] },
63
65
  judge: { cooldownMs: 60000, failuresBeforeCooldown: 3 },
@@ -66,6 +68,7 @@ export function defaultConfig() {
66
68
  steerVisible: false,
67
69
  notices: false,
68
70
  steerBudget: 3,
71
+ steers: { adaptive: true, minSteers: 30, minFollowed: 0.2, maxDisputed: 0.4, recheckEvery: 30, probeEvery: 5 },
69
72
  learning: { adaptiveThresholds: true, patternAnalysis: true, minHoldsForAdaptive: 20, adaptationRate: 0.1, retentionDays: 365 },
70
73
  conscience: {
71
74
  enabled: false,
@@ -82,7 +85,8 @@ export function defaultConfig() {
82
85
  advanceThreshold: 0.70,
83
86
  loadThreshold: 1.0,
84
87
  },
85
- prefs: { enabled: true, inject: false },
88
+ prefs: { enabled: true, inject: true },
89
+ waste: { enabled: true, tip: false, every: 20, sleep: true, paging: true, search: true, recheck: true },
86
90
  };
87
91
  }
88
92
  /** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
@@ -284,6 +288,7 @@ function applyAction(base, raw, timeoutMs, source) {
284
288
  offTask: offTaskThreshold(raw.offTask, base.offTask),
285
289
  intentMismatch: probability(raw.intentMismatch, base.intentMismatch),
286
290
  visibleMismatch: Math.min(probability(raw.visibleMismatch, base.visibleMismatch), probability(raw.intentMismatch, base.intentMismatch)),
291
+ intentTraceOnly: raw.intentTraceOnly === "invisible" || raw.intentTraceOnly === "all" || raw.intentTraceOnly === "none" ? raw.intentTraceOnly : base.intentTraceOnly,
287
292
  shouldProceed: {
288
293
  hold: probability(raw.shouldProceed?.hold, base.shouldProceed.hold),
289
294
  steer: boolean(raw.shouldProceed?.steer, base.shouldProceed.steer),
@@ -315,6 +320,7 @@ function applyStuck(base, raw) {
315
320
  churnThreshold: Math.min(window, positiveInteger(raw.churnThreshold, base.churnThreshold)),
316
321
  nudge: boolean(raw.nudge, base.nudge),
317
322
  repeatSteer: boolean(raw.repeatSteer, base.repeatSteer),
323
+ evidence: boolean(raw.evidence, base.evidence),
318
324
  diffLimit: positiveInteger(raw.diffLimit, base.diffLimit),
319
325
  tailLimit: positiveInteger(raw.tailLimit, base.tailLimit),
320
326
  };
@@ -400,6 +406,7 @@ function applyRules(base, raw) {
400
406
  return {
401
407
  enabled: boolean(raw.enabled, base.enabled),
402
408
  threshold: probability(raw.threshold, base.threshold),
409
+ softThreshold: probability(raw.softThreshold, base.softThreshold),
403
410
  files: globList(raw.files, base.files),
404
411
  fallback: boolean(raw.fallback, base.fallback),
405
412
  maxChars: Math.max(500, positiveInteger(raw.maxChars, base.maxChars)),
@@ -440,6 +447,7 @@ function applyShared(base, raw) {
440
447
  }
441
448
  function applyGuards(base, raw, timeoutMs, source) {
442
449
  return {
450
+ waste: applyWaste(base.waste, raw.waste),
443
451
  rules: applyRules(base.rules, raw.rules),
444
452
  runaway: applyRunaway(base.runaway, raw.runaway),
445
453
  subagent: applySubagent(base.subagent, raw.subagent),
@@ -464,6 +472,8 @@ function applyGuards(base, raw, timeoutMs, source) {
464
472
  recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
465
473
  formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
466
474
  compactAppendix: boolean(raw.context.compactAppendix, base.context.compactAppendix),
475
+ dedupeRuns: boolean(raw.context.dedupeRuns, base.context.dedupeRuns),
476
+ dedupeMessages: boolean(raw.context.dedupeMessages, base.context.dedupeMessages),
467
477
  largeOutput: isObject(raw.context.largeOutput) ? {
468
478
  enabled: boolean(raw.context.largeOutput.enabled, base.context.largeOutput.enabled),
469
479
  threshold: probability(raw.context.largeOutput.threshold, base.context.largeOutput.threshold),
@@ -487,16 +497,42 @@ export function applyUserOverrides(base, raw) {
487
497
  steerVisible: boolean(raw.steerVisible, base.steerVisible),
488
498
  notices: boolean(raw.notices, base.notices),
489
499
  steerBudget: typeof raw.steerBudget === "number" && Number.isInteger(raw.steerBudget) && raw.steerBudget >= 0 ? raw.steerBudget : base.steerBudget,
500
+ steers: applySteers(base.steers, raw.steers),
490
501
  learning: applyLearning(base.learning, raw.learning),
491
502
  conscience: applyConscience(base.conscience, raw.conscience),
492
503
  prefs: applyPrefs(base.prefs, raw.prefs),
493
504
  };
494
505
  }
506
+ function applyWaste(base, raw) {
507
+ if (!isObject(raw))
508
+ return base;
509
+ return {
510
+ enabled: boolean(raw.enabled, base.enabled),
511
+ tip: boolean(raw.tip, base.tip),
512
+ every: Math.max(2, positiveInteger(raw.every, base.every)),
513
+ sleep: boolean(raw.sleep, base.sleep),
514
+ paging: boolean(raw.paging, base.paging),
515
+ search: boolean(raw.search, base.search),
516
+ recheck: boolean(raw.recheck, base.recheck),
517
+ };
518
+ }
495
519
  function applyPrefs(base, raw) {
496
520
  if (!isObject(raw))
497
521
  return base;
498
522
  return { enabled: boolean(raw.enabled, base.enabled), inject: boolean(raw.inject, base.inject) };
499
523
  }
524
+ function applySteers(base, raw) {
525
+ if (!isObject(raw))
526
+ return base;
527
+ return {
528
+ adaptive: boolean(raw.adaptive, base.adaptive),
529
+ minSteers: positiveInteger(raw.minSteers, base.minSteers),
530
+ minFollowed: probability(raw.minFollowed, base.minFollowed),
531
+ maxDisputed: probability(raw.maxDisputed, base.maxDisputed),
532
+ recheckEvery: positiveInteger(raw.recheckEvery, base.recheckEvery),
533
+ probeEvery: positiveInteger(raw.probeEvery, base.probeEvery),
534
+ };
535
+ }
500
536
  function applyLearning(base, raw) {
501
537
  if (!isObject(raw))
502
538
  return base;
@@ -553,11 +589,7 @@ export function readUserConfig() {
553
589
  }
554
590
  export function writeUserConfig(raw) {
555
591
  const path = userConfigPath();
556
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
557
- const temporary = `${path}.${process.pid}.tmp`;
558
- writeFileSync(temporary, `${JSON.stringify(raw, null, 2)}\n`, { mode: 0o600, flag: "w" });
559
- chmodSync(temporary, 0o600);
560
- renameSync(temporary, path);
592
+ writeFileAtomicSync(path, `${JSON.stringify(raw, null, 2)}\n`);
561
593
  return path;
562
594
  }
563
595
  /** Persists one top-level user setting without disturbing the rest of the file. */
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Repeated-run detection: a new tool result or message that pastes again a long block the agent already has in context
3
+ * (a relayed report that quotes every earlier turn) keeps only its new part. Exact line matches only, after trimming
4
+ * trailing spaces; near-duplicates are never collapsed. Deterministic; no requests.
5
+ */
6
+ /** A run shorter than this, in lines or characters, is kept: a short repeat costs less than the pointer that replaces it. */
7
+ export declare const RUN_MIN_LINES = 20;
8
+ export declare const RUN_MIN_CHARS = 1500;
9
+ /** Errors and verdicts usually sit at the end of a result, so the last characters are never replaced. */
10
+ export declare const TAIL_KEEP_CHARS = 2000;
11
+ export interface RepeatedRun {
12
+ /** First line of the run in the new text, 0-based. */
13
+ start: number;
14
+ lines: number;
15
+ chars: number;
16
+ /** Where the earlier copy is, e.g. "an earlier bash result". */
17
+ where: string;
18
+ }
19
+ /** One text the agent has in context, with a stable identity so a sync adds it once. */
20
+ export interface SeenItem {
21
+ key: unknown;
22
+ where: string;
23
+ texts: string[];
24
+ }
25
+ export declare class SeenText {
26
+ /** Interned normalized lines; ids start at 1 so 0 marks a line never seen. */
27
+ private readonly lineIds;
28
+ private readonly docs;
29
+ private readonly windows;
30
+ private readonly keys;
31
+ clear(): void;
32
+ /** Match the index to what is in context now: a compaction or a move to another branch drops texts, so it rebuilds. */
33
+ sync(items: SeenItem[]): void;
34
+ add(text: string, where: string): void;
35
+ /** Runs of `text` that repeat one earlier text exactly, longest match first at each position, outside the kept tail. */
36
+ find(text: string): RepeatedRun[];
37
+ }
38
+ /** Replaces each run with one pointer line; `path` holds the full original text. */
39
+ export declare function collapseRuns(text: string, runs: RepeatedRun[], path: string): string;
40
+ /** The texts a session entry puts in context and where the agent saw them. Thinking, images, and summaries are not indexed. */
41
+ export declare function seenItem(entry: unknown): SeenItem | undefined;
package/dist/dedupe.js ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Repeated-run detection: a new tool result or message that pastes again a long block the agent already has in context
3
+ * (a relayed report that quotes every earlier turn) keeps only its new part. Exact line matches only, after trimming
4
+ * trailing spaces; near-duplicates are never collapsed. Deterministic; no requests.
5
+ */
6
+ /** A run shorter than this, in lines or characters, is kept: a short repeat costs less than the pointer that replaces it. */
7
+ export const RUN_MIN_LINES = 20;
8
+ export const RUN_MIN_CHARS = 1500;
9
+ /** Errors and verdicts usually sit at the end of a result, so the last characters are never replaced. */
10
+ export const TAIL_KEEP_CHARS = 2000;
11
+ const BASE = 1_000_003;
12
+ /** BASE^(RUN_MIN_LINES - 1) mod 2^32, for the rolling window hash. */
13
+ const TOP = (() => { let power = 1; for (let index = 1; index < RUN_MIN_LINES; index++)
14
+ power = Math.imul(power, BASE); return power; })();
15
+ /** Several earlier copies of one window are enough to find the longest match; more only cost memory. */
16
+ const MAX_POSITIONS = 4;
17
+ /** Hash of every RUN_MIN_LINES-line window, by start line. */
18
+ function windowHashes(ids) {
19
+ const hashes = [];
20
+ if (ids.length < RUN_MIN_LINES)
21
+ return hashes;
22
+ let hash = 0;
23
+ for (let index = 0; index < RUN_MIN_LINES; index++)
24
+ hash = (Math.imul(hash, BASE) + ids[index]) | 0;
25
+ hashes.push(hash);
26
+ for (let index = RUN_MIN_LINES; index < ids.length; index++) {
27
+ hash = (Math.imul((hash - Math.imul(ids[index - RUN_MIN_LINES], TOP)) | 0, BASE) + ids[index]) | 0;
28
+ hashes.push(hash);
29
+ }
30
+ return hashes;
31
+ }
32
+ export class SeenText {
33
+ /** Interned normalized lines; ids start at 1 so 0 marks a line never seen. */
34
+ lineIds = new Map();
35
+ docs = [];
36
+ windows = new Map();
37
+ keys = new Set();
38
+ clear() {
39
+ this.lineIds.clear();
40
+ this.docs.length = 0;
41
+ this.windows.clear();
42
+ this.keys.clear();
43
+ }
44
+ /** Match the index to what is in context now: a compaction or a move to another branch drops texts, so it rebuilds. */
45
+ sync(items) {
46
+ const current = new Set(items.map(item => item.key));
47
+ for (const key of this.keys)
48
+ if (!current.has(key)) {
49
+ this.clear();
50
+ break;
51
+ }
52
+ for (const item of items) {
53
+ if (this.keys.has(item.key))
54
+ continue;
55
+ this.keys.add(item.key);
56
+ for (const text of item.texts)
57
+ this.add(text, item.where);
58
+ }
59
+ }
60
+ add(text, where) {
61
+ const lines = text.split("\n");
62
+ if (lines.length < RUN_MIN_LINES)
63
+ return;
64
+ const ids = lines.map(line => {
65
+ const normalized = line.trimEnd();
66
+ let id = this.lineIds.get(normalized);
67
+ if (id === undefined) {
68
+ id = this.lineIds.size + 1;
69
+ this.lineIds.set(normalized, id);
70
+ }
71
+ return id;
72
+ });
73
+ const doc = this.docs.push({ where, ids }) - 1;
74
+ windowHashes(ids).forEach((hash, line) => {
75
+ const positions = this.windows.get(hash);
76
+ if (!positions)
77
+ this.windows.set(hash, [[doc, line]]);
78
+ else if (positions.length < MAX_POSITIONS)
79
+ positions.push([doc, line]);
80
+ });
81
+ }
82
+ /** Runs of `text` that repeat one earlier text exactly, longest match first at each position, outside the kept tail. */
83
+ find(text) {
84
+ const lines = text.split("\n");
85
+ const cutoff = text.length - TAIL_KEEP_CHARS;
86
+ let limit = 0;
87
+ for (let offset = 0; limit < lines.length; limit++) {
88
+ const end = offset + lines[limit].length + 1;
89
+ if (end > cutoff)
90
+ break;
91
+ offset = end;
92
+ }
93
+ if (limit < RUN_MIN_LINES)
94
+ return [];
95
+ const ids = lines.slice(0, limit).map(line => this.lineIds.get(line.trimEnd()) ?? 0);
96
+ const hashes = windowHashes(ids);
97
+ const runs = [];
98
+ let index = 0;
99
+ while (index + RUN_MIN_LINES <= limit) {
100
+ let best;
101
+ for (const [doc, line] of this.windows.get(hashes[index]) ?? []) {
102
+ const seen = this.docs[doc].ids;
103
+ let length = 0;
104
+ while (index + length < limit && line + length < seen.length && ids[index + length] !== 0 && ids[index + length] === seen[line + length])
105
+ length++;
106
+ if (length >= RUN_MIN_LINES && (!best || length > best.length))
107
+ best = { doc, length };
108
+ }
109
+ if (best) {
110
+ let chars = 0;
111
+ for (let line = index; line < index + best.length; line++)
112
+ chars += lines[line].length + 1;
113
+ if (chars >= RUN_MIN_CHARS) {
114
+ runs.push({ start: index, lines: best.length, chars, where: this.docs[best.doc].where });
115
+ index += best.length;
116
+ continue;
117
+ }
118
+ }
119
+ index++;
120
+ }
121
+ return runs;
122
+ }
123
+ }
124
+ /** Replaces each run with one pointer line; `path` holds the full original text. */
125
+ export function collapseRuns(text, runs, path) {
126
+ if (!runs.length)
127
+ return text;
128
+ const lines = text.split("\n");
129
+ const kept = [];
130
+ let index = 0;
131
+ for (const run of runs) {
132
+ kept.push(...lines.slice(index, run.start), `[pi-warden: the next ${run.lines} lines repeat ${run.where} — omitted; full text: ${path}]`);
133
+ index = run.start + run.lines;
134
+ }
135
+ kept.push(...lines.slice(index));
136
+ return kept.join("\n");
137
+ }
138
+ function textsOf(content) {
139
+ if (typeof content === "string")
140
+ return [content];
141
+ if (!Array.isArray(content))
142
+ return [];
143
+ return content.filter(part => part?.type === "text" && typeof part.text === "string").map(part => part.text);
144
+ }
145
+ /** The texts a session entry puts in context and where the agent saw them. Thinking, images, and summaries are not indexed. */
146
+ export function seenItem(entry) {
147
+ if (!entry || typeof entry !== "object")
148
+ return undefined;
149
+ const record = entry;
150
+ const key = typeof record.id === "string" ? record.id : entry;
151
+ if (record.type === "custom_message")
152
+ return { key, where: `an earlier ${String(record.customType ?? "custom")} message`, texts: textsOf(record.content) };
153
+ if (record.type !== "message" || !record.message)
154
+ return undefined;
155
+ const { role } = record.message;
156
+ const where = role === "toolResult" ? `an earlier ${String(record.message.toolName ?? "tool")} result`
157
+ : role === "user" ? "an earlier user message"
158
+ : role === "assistant" ? "an earlier assistant reply"
159
+ : role === "custom" ? `an earlier ${String(record.message.customType ?? "custom")} message`
160
+ : undefined;
161
+ return where ? { key, where, texts: textsOf(record.message.content) } : undefined;
162
+ }
@@ -0,0 +1,93 @@
1
+ import type { Attempt } from "./stuck.js";
2
+ /** Byte cap for the whole `evidence` object; over it, the oldest runs go first, then the oldest edits. */
3
+ export declare const EVIDENCE_LIMIT = 4096;
4
+ /** A failed or check output parsed into failing tests, error lines, a location, a summary and an exit code. */
5
+ export type ParsedFailure = {
6
+ format: string;
7
+ failing: string[];
8
+ failing_more?: number;
9
+ errors: string[];
10
+ location?: string;
11
+ summary?: string;
12
+ exit?: number;
13
+ };
14
+ /** Parses a failed or check output into failing tests, error lines, a location, a summary and an exit code. */
15
+ export declare function parseFailure(text: string): ParsedFailure;
16
+ /**
17
+ * Normalises what changes between identical runs: durations, clock times and dates, addresses, long numbers, thread
18
+ * ids, temp paths, and line:column positions. Other digits stay, so "expected 3" and "expected 4" still differ.
19
+ */
20
+ export declare function normaliseFailureText(value: string | undefined): string;
21
+ /**
22
+ * Order-free signature of a failure: which tests failed and with which errors, both normalised. Pass counts are left
23
+ * out, so a run narrowed by a flag (`-x`, a test filter) still matches the full run that failed the same way.
24
+ */
25
+ export declare function failureSignature(parsed: ParsedFailure, raw?: string): string;
26
+ /** The edit pairs of an `edit` input, as `[oldText, newText]`: the `edits` array, or one top-level pair. */
27
+ export declare function editPairs(input: Record<string, unknown>): Array<[string, string]>;
28
+ /** Removed and added lines of one edit, as a multiset difference, so unchanged context lines drop out. */
29
+ export declare function lineDelta(oldText: string, newText: string): {
30
+ removed: string[];
31
+ added: string[];
32
+ };
33
+ /** What an `edit` or `write` changed: the path, the line counts, and a diff capped at `DIFF_CAP` characters. */
34
+ export type EditDiff = {
35
+ path: string;
36
+ added: number;
37
+ removed: number;
38
+ removedLines: string[];
39
+ addedLines: string[];
40
+ diff: string;
41
+ };
42
+ /**
43
+ * The change an `edit` or `write` made. Every string is as the agent wrote it: the caller redacts what leaves the
44
+ * machine. The diff is the whole added and removed text, not a line-by-line patch, because the judge compares one
45
+ * change against the next.
46
+ */
47
+ export declare function editDiff(tool: string, input: Record<string, unknown>): EditDiff;
48
+ /** One run in the evidence: a failed call, or a call that ran a test, build or lint command and passed. */
49
+ export type EvidenceRun = {
50
+ n: number;
51
+ outcome: "failed" | "ok";
52
+ parser?: string;
53
+ failing_tests?: string[];
54
+ failing_tests_more?: number;
55
+ errors?: string[];
56
+ location?: string;
57
+ summary?: string;
58
+ output_tail?: string;
59
+ output_head_tail?: string;
60
+ /** The run number of the earlier run whose failure matches, after normalising times, temp paths and line numbers. */
61
+ same_failure_as_run?: number | null;
62
+ exit_code?: number;
63
+ };
64
+ /** One `edit`/`write` call in the evidence. */
65
+ export type EvidenceEdit = {
66
+ n: number;
67
+ path: string;
68
+ added_lines: number;
69
+ removed_lines: number;
70
+ diff: string;
71
+ };
72
+ /** The counts a judge can weigh without reading every run. */
73
+ export type EvidenceDigest = {
74
+ failed_runs: number;
75
+ distinct_failures: number;
76
+ /** Runs whose command repeats an earlier run's command, whatever the outcome. */
77
+ same_command_runs: number[];
78
+ latest_failure_seen_before: boolean;
79
+ edits_between_failed_runs: number;
80
+ information_calls_between_failed_runs: number;
81
+ };
82
+ export type StuckEvidence = {
83
+ runs: EvidenceRun[];
84
+ edits: EvidenceEdit[];
85
+ digest: EvidenceDigest;
86
+ };
87
+ /**
88
+ * Builds the evidence for one stuck window: a run per failed call and per passing check command, an edit per
89
+ * `edit`/`write`, and a digest. The oldest runs are dropped first when the whole object is over `EVIDENCE_LIMIT`
90
+ * bytes, then the oldest edits; the digest is then recomputed over what is left, and a run that pointed at a dropped
91
+ * run points at nothing. The floor is one run plus the digest, which is far under the cap.
92
+ */
93
+ export declare function buildStuckEvidence(attempts: readonly Attempt[], limit?: number): StuckEvidence;