pi-blackhole 0.2.0 → 0.2.2

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.
@@ -1,237 +0,0 @@
1
- import type { Message } from "@earendil-works/pi-ai";
2
- import { buildSections } from "./build-sections";
3
- import { clip } from "./content";
4
- import { normalize } from "./normalize";
5
- import { renderMessage } from "./render-entries";
6
- import { searchEntries } from "./search-entries";
7
- import { type CompileInput, compile } from "./summarize";
8
-
9
- const SECTION_HEADERS = ["Session Goal", "Files And Changes", "Commits", "Outstanding Context"];
10
-
11
- interface RoleCounts {
12
- user: number;
13
- assistant: number;
14
- toolResult: number;
15
- }
16
-
17
- interface BlockCounts {
18
- user: number;
19
- assistant: number;
20
- toolCalls: number;
21
- toolResults: number;
22
- thinking: number;
23
- }
24
-
25
- export interface RecallProbe {
26
- label: string;
27
- sourceText: string;
28
- query: string;
29
- summaryMentioned: boolean;
30
- recallHits: number;
31
- }
32
-
33
- export interface CompactReport {
34
- summary: string;
35
- before: {
36
- messageCount: number;
37
- roleCounts: RoleCounts;
38
- blockCounts: BlockCounts;
39
- inputChars: number;
40
- estimatedTokens: number;
41
- topFiles: string[];
42
- preview: string;
43
- };
44
- after: {
45
- summaryLength: number;
46
- estimatedTokens: number;
47
- sectionCount: number;
48
- summaryPreview: string;
49
- goalsCount: number;
50
- blockersCount: number;
51
- briefTranscriptLines: number;
52
- };
53
- compression: {
54
- charsBefore: number;
55
- charsAfter: number;
56
- ratio: number;
57
- messagesBefore: number;
58
- };
59
- recall: {
60
- probes: RecallProbe[];
61
- };
62
- }
63
-
64
- const estimateTokensFromChars = (chars: number): number =>
65
- Math.ceil(chars / 4);
66
-
67
- const countRoles = (messages: Message[]): RoleCounts => {
68
- const counts: RoleCounts = { user: 0, assistant: 0, toolResult: 0 };
69
- for (const msg of messages) {
70
- if (msg.role === "user") counts.user += 1;
71
- else if (msg.role === "assistant") counts.assistant += 1;
72
- else if (msg.role === "toolResult") counts.toolResult += 1;
73
- }
74
- return counts;
75
- };
76
-
77
- const countBlocks = (messages: Message[]): BlockCounts => {
78
- const counts: BlockCounts = {
79
- user: 0,
80
- assistant: 0,
81
- toolCalls: 0,
82
- toolResults: 0,
83
- thinking: 0,
84
- };
85
-
86
- for (const block of normalize(messages)) {
87
- if (block.kind === "user") counts.user += 1;
88
- else if (block.kind === "assistant") counts.assistant += 1;
89
- else if (block.kind === "tool_call") counts.toolCalls += 1;
90
- else if (block.kind === "tool_result") counts.toolResults += 1;
91
- else if (block.kind === "thinking") counts.thinking += 1;
92
- }
93
-
94
- return counts;
95
- };
96
-
97
- const inputCharsOf = (messages: Message[]): number =>
98
- messages
99
- .map((msg, index) => renderMessage(msg, index, "", true).summary.length)
100
- .reduce((sum, len) => sum + len, 0);
101
-
102
- const topFilesOf = (messages: Message[]): string[] => {
103
- const files = new Set<string>();
104
- for (const block of normalize(messages)) {
105
- if (block.kind === "tool_call") {
106
- for (const key of ["path", "file_path", "filePath", "file"]) {
107
- const val = block.args[key];
108
- if (typeof val === "string") { files.add(val); break; }
109
- }
110
- }
111
- }
112
- return [...files].slice(0, 10);
113
- };
114
-
115
- const previewOf = (messages: Message[], edgeCount = 3): string => {
116
- const rendered = messages.map((msg, index) => renderMessage(msg, index, ""));
117
- if (rendered.length === 0) return "(empty)";
118
- if (rendered.length <= edgeCount * 2) {
119
- return rendered
120
- .map((entry) => `#${entry.index} [${entry.role}] ${clip(entry.summary, 220)}`)
121
- .join("\n");
122
- }
123
-
124
- const first = rendered.slice(0, edgeCount);
125
- const last = rendered.slice(-edgeCount);
126
- return [
127
- ...first.map((entry) => `#${entry.index} [${entry.role}] ${clip(entry.summary, 220)}`),
128
- "...",
129
- ...last.map((entry) => `#${entry.index} [${entry.role}] ${clip(entry.summary, 220)}`),
130
- ].join("\n");
131
- };
132
-
133
- const sectionCountOf = (summary: string): number =>
134
- SECTION_HEADERS.filter((header) => summary.includes(`[${header}]`)).length;
135
-
136
- const briefLineCountOf = (summary: string): number => {
137
- const sep = "\n\n---\n\n";
138
- const idx = summary.indexOf(sep);
139
- if (idx < 0) return 0;
140
- return summary.slice(idx + sep.length).split("\n").length;
141
- };
142
-
143
- const queryTermsOf = (text: string): string[] =>
144
- (text.match(/[\p{L}\p{N}_./-]{3,}/gu) ?? [])
145
- .map((part) => part.trim())
146
- .filter(Boolean);
147
-
148
- const queryOf = (text: string): string => {
149
- const terms = queryTermsOf(text);
150
- return terms.slice(0, 6).join(" ");
151
- };
152
-
153
- const matchesQuery = (text: string, query: string): boolean => {
154
- const hay = text.toLowerCase();
155
- return query
156
- .toLowerCase()
157
- .split(/\s+/)
158
- .filter(Boolean)
159
- .every((term) => hay.includes(term));
160
- };
161
-
162
- const probesOf = (messages: Message[], summary: string): RecallProbe[] => {
163
- const blocks = normalize(messages);
164
- const data = buildSections({ blocks });
165
-
166
- // Find first file from tool calls
167
- let firstFile = "";
168
- for (const b of blocks) {
169
- if (b.kind === "tool_call") {
170
- for (const key of ["path", "file_path", "filePath", "file"]) {
171
- if (typeof b.args[key] === "string") { firstFile = b.args[key] as string; break; }
172
- }
173
- if (firstFile) break;
174
- }
175
- }
176
-
177
- const rawProbes = [
178
- { label: "goal", text: data.sessionGoal[0] ?? "" },
179
- { label: "file", text: firstFile },
180
- { label: "problem", text: data.outstandingContext[0] ?? "" },
181
- ];
182
-
183
- const rendered = messages.map((msg, index) => renderMessage(msg, index, ""));
184
-
185
- return rawProbes
186
- .map(({ label, text }) => {
187
- const sourceText = text.trim();
188
- const query = queryOf(sourceText);
189
- if (!query) return null;
190
- return {
191
- label,
192
- sourceText,
193
- query,
194
- summaryMentioned: matchesQuery(summary, query),
195
- recallHits: searchEntries(rendered, messages, query).length,
196
- };
197
- })
198
- .filter((probe): probe is RecallProbe => probe !== null);
199
- };
200
-
201
- export const buildCompactReport = (input: CompileInput): CompactReport => {
202
- const summary = compile(input);
203
- const data = buildSections({ blocks: normalize(input.messages) });
204
- const inputChars = inputCharsOf(input.messages);
205
- const topFiles = topFilesOf(input.messages);
206
-
207
- return {
208
- summary,
209
- before: {
210
- messageCount: input.messages.length,
211
- roleCounts: countRoles(input.messages),
212
- blockCounts: countBlocks(input.messages),
213
- inputChars,
214
- estimatedTokens: estimateTokensFromChars(inputChars),
215
- topFiles,
216
- preview: previewOf(input.messages),
217
- },
218
- after: {
219
- summaryLength: summary.length,
220
- estimatedTokens: estimateTokensFromChars(summary.length),
221
- sectionCount: sectionCountOf(summary),
222
- summaryPreview: summary,
223
- goalsCount: data.sessionGoal.length,
224
- blockersCount: data.outstandingContext.length,
225
- briefTranscriptLines: briefLineCountOf(summary),
226
- },
227
- compression: {
228
- charsBefore: inputChars,
229
- charsAfter: summary.length,
230
- ratio: summary.length === 0 ? 0 : Number((inputChars / summary.length).toFixed(2)),
231
- messagesBefore: input.messages.length,
232
- },
233
- recall: {
234
- probes: probesOf(input.messages, summary),
235
- },
236
- };
237
- };
@@ -1,63 +0,0 @@
1
- /**
2
- * Observational memory compaction hook — injects OM content into pi-vcc summaries.
3
- *
4
- * Upstream: https://github.com/elpapi42/pi-observational-memory (src/hooks/compaction-hook.ts)
5
- * NOTE: This hook is intentionally NOT registered in index.ts.
6
- * The joining point is in src/hooks/before-compact.ts which handles both
7
- * pi-vcc compilation AND OM projection in a single pass.
8
- * Kept as reference for the original standalone behavior.
9
- */
10
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
-
12
- import type { Runtime } from "./runtime.js";
13
- import { buildCompactionProjection, renderSummary, type Entry } from "./ledger/index.js";
14
-
15
- const DEFAULT_OBSERVATIONS_POOL_MAX_TOKENS = 20_000;
16
-
17
- function observationsPoolMaxTokens(runtime: Runtime): number {
18
- const value = (runtime.config as { observationsPoolMaxTokens?: unknown }).observationsPoolMaxTokens;
19
- return typeof value === "number" && Number.isFinite(value) && value > 0
20
- ? value
21
- : DEFAULT_OBSERVATIONS_POOL_MAX_TOKENS;
22
- }
23
-
24
- export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void {
25
- pi.on("session_before_compact", async (event: any, ctx: any) => {
26
- // Dead code — this hook is intentionally not registered in index.ts.
27
- // Kept as reference for the original observational-memory behavior.
28
- if ((runtime.config as any).disableCompactionHook) return;
29
- if (runtime.compactHookInFlight) {
30
- if (ctx.hasUI) {
31
- ctx.ui.notify(
32
- "Observational memory: another compaction is already in progress; cancelling duplicate",
33
- "warning",
34
- );
35
- }
36
- return { cancel: true };
37
- }
38
-
39
- runtime.compactHookInFlight = true;
40
- try {
41
- runtime.ensureConfig(ctx.cwd);
42
- const { preparation, branchEntries } = event;
43
- const { firstKeptEntryId, tokensBefore } = preparation;
44
- const projection = buildCompactionProjection(
45
- branchEntries as Entry[],
46
- firstKeptEntryId,
47
- { observationsPoolMaxTokens: observationsPoolMaxTokens(runtime) },
48
- );
49
- const summary = renderSummary(projection.reflections, projection.observations);
50
-
51
- return {
52
- compaction: {
53
- summary,
54
- firstKeptEntryId,
55
- tokensBefore,
56
- details: projection.details,
57
- },
58
- };
59
- } finally {
60
- runtime.compactHookInFlight = false;
61
- }
62
- });
63
- }