pi-precompact 0.0.0-stage → 0.2.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.
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ buildSessionProjection,
4
+ type CompactionResult,
5
+ estimateTokens,
6
+ type SessionEntry,
7
+ SessionManager,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import type { Preparation } from "./native.ts";
10
+
11
+ export interface Snapshot {
12
+ sessionId: string;
13
+ identity: string;
14
+ entryCount: number;
15
+ branchHash: string;
16
+ preparation: Preparation;
17
+ }
18
+
19
+ export function fingerprint(value: unknown): string {
20
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
21
+ }
22
+
23
+ export function snapshotValid(
24
+ snapshot: Snapshot,
25
+ sessionId: string,
26
+ identity: string,
27
+ branch: SessionEntry[],
28
+ ): boolean {
29
+ if (
30
+ sessionId !== snapshot.sessionId ||
31
+ identity !== snapshot.identity ||
32
+ branch.length < snapshot.entryCount
33
+ )
34
+ return false;
35
+ if (fingerprint(branch.slice(0, snapshot.entryCount)) !== snapshot.branchHash) return false;
36
+ // Even an edit to a retained entry can change a split-turn summary. Fail closed.
37
+ return !branch
38
+ .slice(snapshot.entryCount)
39
+ .some(
40
+ (entry) =>
41
+ entry.type === "context_edit" ||
42
+ entry.type === "compaction" ||
43
+ entry.type === "branch_summary" ||
44
+ entry.type === "model_change",
45
+ );
46
+ }
47
+
48
+ /** Preview exactly the entry Pi will append, preserving native messages and attachments. */
49
+ export function validateResult(
50
+ snapshot: Snapshot,
51
+ result: CompactionResult,
52
+ branch: SessionEntry[],
53
+ contextWindow: number,
54
+ tokensBefore: number,
55
+ ): CompactionResult | undefined {
56
+ if (!result.summary.trim() || result.firstKeptEntryId !== snapshot.preparation.firstKeptEntryId) return;
57
+ if (!branch.some((entry) => entry.id === result.firstKeptEntryId)) return;
58
+ const ceiling = contextWindow - snapshot.preparation.settings.reserveTokens;
59
+ const tailLimit = Math.min(
60
+ Math.max(2 * snapshot.preparation.settings.keepRecentTokens, 30_000),
61
+ ceiling / 4,
62
+ );
63
+ const addedIds = new Set(branch.slice(snapshot.entryCount).map((entry) => entry.id));
64
+ const growth = buildSessionProjection(branch)
65
+ .entries.filter((entry) => addedIds.has(entry.sourceEntry.id))
66
+ .flatMap((entry) => entry.messages)
67
+ .reduce((sum, message) => sum + estimateTokens(message), 0);
68
+ if (growth > tailLimit) return;
69
+ const preview = SessionManager.inMemory(undefined, undefined, structuredClone(branch));
70
+ preview.appendCompaction(result.summary, result.firstKeptEntryId, tokensBefore, result.details, true);
71
+ const estimatedTokensAfter = preview
72
+ .buildSessionProjection()
73
+ .messages.reduce((sum, message) => sum + estimateTokens(message), 0);
74
+ // Token estimates are heuristic. Leave extra room in addition to Pi's native reserve.
75
+ if (
76
+ !Number.isFinite(estimatedTokensAfter) ||
77
+ estimatedTokensAfter * 1.15 >= ceiling ||
78
+ estimatedTokensAfter >= tokensBefore
79
+ )
80
+ return;
81
+ return { ...result, tokensBefore, estimatedTokensAfter };
82
+ }