pi-blackhole 0.2.0 → 0.2.1
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/README.md +2 -2
- package/package.json +12 -7
- package/src/commands/memory.ts +23 -3
- package/src/commands/pi-vcc.ts +2 -2
- package/src/core/brief.ts +33 -3
- package/src/core/load-messages.ts +5 -1
- package/src/core/settings.ts +1 -7
- package/src/core/unified-config.ts +2 -2
- package/src/hooks/before-compact.ts +11 -26
- package/src/om/compaction-trigger.ts +15 -3
- package/src/om/consolidation.ts +44 -8
- package/src/om/ledger/projection.ts +4 -1
- package/src/om/ledger/recall.ts +3 -3
- package/src/om/reverse-recall.ts +1 -2
- package/src/om/runtime.ts +4 -0
- package/src/core/report.ts +0 -237
- package/src/om/compaction-hook.ts +0 -63
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ Set `memory: false` or run `/blackhole om-off` for pure pi-vcc compaction — no
|
|
|
170
170
|
|
|
171
171
|
## Configuration
|
|
172
172
|
|
|
173
|
-
All settings in **`~/.pi/agent/pi-blackhole-config.json`** — auto-created with defaults on first startup. See [`CONFIG.md`](CONFIG.md) for the full reference and tuning guidance.
|
|
173
|
+
All settings in **`~/.pi/agent/pi-blackhole/pi-blackhole-config.json`** — auto-created with defaults on first startup. See [`CONFIG.md`](CONFIG.md) for the full reference and tuning guidance. An annotated example config with explanations is at [`example-config.json`](example-config.json).
|
|
174
174
|
|
|
175
175
|
Quick start with custom models:
|
|
176
176
|
|
|
@@ -354,7 +354,7 @@ pnpm add pi-blackhole
|
|
|
354
354
|
|
|
355
355
|
```bash
|
|
356
356
|
pi uninstall git:github.com/k0valik/pi-blackhole
|
|
357
|
-
rm -rf ~/.pi/agent/pi-blackhole
|
|
357
|
+
rm -rf ~/.pi/agent/pi-blackhole
|
|
358
358
|
```
|
|
359
359
|
|
|
360
360
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-blackhole",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Unified compaction + observational memory extension for Pi — compresses conversation context while preserving durable observations and reflections",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"type": "git",
|
|
37
37
|
"url": "git+https://github.com/k0valik/pi-blackhole.git"
|
|
38
38
|
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
|
|
41
|
-
"@earendil-works/pi-
|
|
42
|
-
"@earendil-works/pi-
|
|
43
|
-
"@earendil-works/pi-
|
|
44
|
-
"
|
|
39
|
+
"dependencies": {},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@earendil-works/pi-agent-core": ">=0.75.5 <1.0.0",
|
|
42
|
+
"@earendil-works/pi-ai": ">=0.75.4 <1.0.0",
|
|
43
|
+
"@earendil-works/pi-coding-agent": ">=0.75.4 <1.0.0",
|
|
44
|
+
"@earendil-works/pi-tui": ">=0.75.5 <1.0.0"
|
|
45
45
|
},
|
|
46
46
|
"pi": {
|
|
47
47
|
"extensions": [
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@earendil-works/pi-agent-core": "0.75.5",
|
|
53
|
+
"@earendil-works/pi-ai": "0.75.4",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "0.75.4",
|
|
55
|
+
"@earendil-works/pi-tui": "0.75.5",
|
|
56
|
+
"typebox": "^1.1.38",
|
|
52
57
|
"typescript": "^5.7.3",
|
|
53
58
|
"vitest": "^4.1.7"
|
|
54
59
|
}
|
package/src/commands/memory.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { copyTextToClipboard } from "../om/clipboard.js";
|
|
|
9
9
|
import type { Runtime } from "../om/runtime.js";
|
|
10
10
|
import {
|
|
11
11
|
diffProjection,
|
|
12
|
+
entryIndexForId,
|
|
12
13
|
foldLedger,
|
|
13
14
|
fullProjection,
|
|
14
15
|
observationToSummaryLine,
|
|
16
|
+
rawTokensAfterIndex,
|
|
15
17
|
rawTokensSinceDropCoverage,
|
|
16
18
|
rawTokensSinceLastCompaction,
|
|
17
19
|
rawTokensSinceObservationCoverage,
|
|
@@ -125,11 +127,29 @@ export function registerMemoryCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
125
127
|
`Reflections: ${folded.reflections.length} recorded / ${visible.reflections.length} visible`,
|
|
126
128
|
[addedSuffix(drift.reflectionsOnlyInFull.length)],
|
|
127
129
|
);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
let obsProgress = rawTokensSinceObservationCoverage(entries);
|
|
131
|
+
let reflectionProgress = rawTokensSinceReflectionCoverage(entries);
|
|
132
|
+
let dropProgress = rawTokensSinceDropCoverage(entries);
|
|
131
133
|
const compactionProgress = rawTokensSinceLastCompaction(entries);
|
|
132
134
|
|
|
135
|
+
// In noAutoCompact mode, pending coversUpToId entries act as virtual coverage markers
|
|
136
|
+
// that aren't reflected in the branch. Adjust accumulated counts accordingly.
|
|
137
|
+
if (runtime.config.noAutoCompact) {
|
|
138
|
+
const pending = readPendingState(sessionId);
|
|
139
|
+
if (pending.observation?.coversUpToId) {
|
|
140
|
+
const idx = entryIndexForId(entries, pending.observation.coversUpToId);
|
|
141
|
+
if (idx >= 0) obsProgress = rawTokensAfterIndex(entries, idx);
|
|
142
|
+
}
|
|
143
|
+
if (pending.reflection?.coversUpToId) {
|
|
144
|
+
const idx = entryIndexForId(entries, pending.reflection.coversUpToId);
|
|
145
|
+
if (idx >= 0) reflectionProgress = rawTokensAfterIndex(entries, idx);
|
|
146
|
+
}
|
|
147
|
+
if (pending.dropped?.coversUpToId) {
|
|
148
|
+
const idx = entryIndexForId(entries, pending.dropped.coversUpToId);
|
|
149
|
+
if (idx >= 0) dropProgress = rawTokensAfterIndex(entries, idx);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
const passiveLines = runtime.config.passive === true
|
|
134
154
|
? [
|
|
135
155
|
"── Mode ──",
|
package/src/commands/pi-vcc.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { Runtime } from "../om/runtime.js";
|
|
11
|
-
import {
|
|
11
|
+
import { PI_VCC_COMPACT_INSTRUCTION } from "../hooks/before-compact";
|
|
12
12
|
import { saveUnifiedConfig } from "../core/unified-config.js";
|
|
13
13
|
import { readPendingState, clearPendingState, hasPendingData } from "../om/pending.js";
|
|
14
14
|
import {
|
|
@@ -71,7 +71,7 @@ export const registerPiVccCommand = (pi: ExtensionAPI, runtime: Runtime) => {
|
|
|
71
71
|
ctx.compact({
|
|
72
72
|
customInstructions: PI_VCC_COMPACT_INSTRUCTION,
|
|
73
73
|
onComplete: () => {
|
|
74
|
-
const stats =
|
|
74
|
+
const stats = runtime.compactionStats;
|
|
75
75
|
if (stats) {
|
|
76
76
|
ctx.ui.notify(
|
|
77
77
|
`blackhole: ${stats.summarized} source entries processed; tail kept ${stats.kept} (~${formatTokens(stats.keptTokensEst)} tok).`,
|
package/src/core/brief.ts
CHANGED
|
@@ -19,8 +19,38 @@ const isNoiseUser = (text: string): boolean => {
|
|
|
19
19
|
|
|
20
20
|
// ── truncation ──
|
|
21
21
|
|
|
22
|
-
// Unicode-aware word segmentation via Intl.Segmenter
|
|
23
|
-
|
|
22
|
+
// Unicode-aware word segmentation via Intl.Segmenter with lazy init & fallback
|
|
23
|
+
let _segmenter: Intl.Segmenter | null | undefined = undefined;
|
|
24
|
+
const wordSegments = (text: string): Array<{ segment: string; index: number; isWordLike?: boolean }> => {
|
|
25
|
+
// Available: fast path
|
|
26
|
+
if (_segmenter) return Array.from(_segmenter.segment(text));
|
|
27
|
+
// Fallback already established: don't retry the constructor
|
|
28
|
+
if (_segmenter === null) {
|
|
29
|
+
const parts: Array<{ segment: string; index: number; isWordLike?: boolean }> = [];
|
|
30
|
+
let idx = 0;
|
|
31
|
+
for (const part of text.split(/(\s+)/)) {
|
|
32
|
+
if (!part) continue;
|
|
33
|
+
parts.push({ segment: part, index: idx, isWordLike: /\S/.test(part) });
|
|
34
|
+
idx += part.length;
|
|
35
|
+
}
|
|
36
|
+
return parts;
|
|
37
|
+
}
|
|
38
|
+
// _segmenter === undefined: first call — attempt construction
|
|
39
|
+
try {
|
|
40
|
+
_segmenter = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
41
|
+
return Array.from(_segmenter.segment(text));
|
|
42
|
+
} catch {
|
|
43
|
+
_segmenter = null; // permanently fallback
|
|
44
|
+
const parts: Array<{ segment: string; index: number; isWordLike?: boolean }> = [];
|
|
45
|
+
let idx = 0;
|
|
46
|
+
for (const part of text.split(/(\s+)/)) {
|
|
47
|
+
if (!part) continue;
|
|
48
|
+
parts.push({ segment: part, index: idx, isWordLike: /\S/.test(part) });
|
|
49
|
+
idx += part.length;
|
|
50
|
+
}
|
|
51
|
+
return parts;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
24
54
|
|
|
25
55
|
/** Check if segment is a word (Bun's isWordLike is unreliable for alphanumeric tokens) */
|
|
26
56
|
const isWord = (seg: { segment: string; isWordLike?: boolean }): boolean =>
|
|
@@ -47,7 +77,7 @@ const truncateTokens = (text: string, limit: number): string => {
|
|
|
47
77
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
48
78
|
let count = 0;
|
|
49
79
|
let lastEnd = 0;
|
|
50
|
-
for (const seg of
|
|
80
|
+
for (const seg of wordSegments(flat)) {
|
|
51
81
|
if (isWord(seg)) {
|
|
52
82
|
if (!STOP_WORDS.has(seg.segment.toLowerCase())) {
|
|
53
83
|
count++;
|
|
@@ -15,9 +15,13 @@ export const loadAllMessages = (
|
|
|
15
15
|
): LoadedMessages => {
|
|
16
16
|
const content = readFileSync(sessionFile, "utf-8");
|
|
17
17
|
const entries: any[] = [];
|
|
18
|
+
let parseErrors = 0;
|
|
18
19
|
for (const line of content.split("\n")) {
|
|
19
20
|
if (!line.trim()) continue;
|
|
20
|
-
try { entries.push(JSON.parse(line)); } catch {}
|
|
21
|
+
try { entries.push(JSON.parse(line)); } catch { parseErrors++; }
|
|
22
|
+
}
|
|
23
|
+
if (parseErrors > 0) {
|
|
24
|
+
console.warn(`blackhole: ${parseErrors} malformed JSONL line(s) in ${sessionFile}`);
|
|
21
25
|
}
|
|
22
26
|
const rendered: RenderedEntry[] = [];
|
|
23
27
|
const rawMessages: Message[] = [];
|
package/src/core/settings.ts
CHANGED
|
@@ -13,11 +13,6 @@ export interface PiVccSettings {
|
|
|
13
13
|
debug: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const DEFAULT_SETTINGS: PiVccSettings = {
|
|
17
|
-
overrideDefaultCompaction: false,
|
|
18
|
-
debug: false,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
16
|
export function loadSettings(): PiVccSettings {
|
|
22
17
|
const config = loadUnifiedConfig(process.cwd());
|
|
23
18
|
return {
|
|
@@ -30,5 +25,4 @@ export function scaffoldSettings(): void {
|
|
|
30
25
|
scaffoldConfig();
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
export { loadUnifiedConfig as loadConfig } from "./unified-config.js";
|
|
28
|
+
|
|
@@ -11,30 +11,19 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { writeFileSync } from "fs";
|
|
13
13
|
import { compile } from "../core/summarize";
|
|
14
|
-
import { loadSettings, type PiVccSettings } from "../core/settings";
|
|
15
14
|
import type { PiVccCompactionDetails } from "../details";
|
|
16
15
|
import { buildCompactionProjection, renderSummary } from "../om/ledger/index.js";
|
|
17
16
|
import type { Runtime } from "../om/runtime.js";
|
|
18
17
|
|
|
19
18
|
export const PI_VCC_COMPACT_INSTRUCTION = "__pi_vcc__";
|
|
20
19
|
|
|
21
|
-
export interface CompactionStats {
|
|
22
|
-
summarized: number;
|
|
23
|
-
kept: number;
|
|
24
|
-
keptTokensEst: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let lastStats: CompactionStats | null = null;
|
|
28
|
-
let lastCompactWasPiVcc = false;
|
|
29
|
-
export const getLastCompactionStats = () => lastStats;
|
|
30
|
-
|
|
31
20
|
const formatTokens = (n: number): string => {
|
|
32
21
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
33
22
|
return String(n);
|
|
34
23
|
};
|
|
35
24
|
|
|
36
|
-
const dbg = (
|
|
37
|
-
if (!
|
|
25
|
+
const dbg = (debug: boolean, data: Record<string, unknown>) => {
|
|
26
|
+
if (!debug) return;
|
|
38
27
|
try { writeFileSync("/tmp/pi-blackhole-debug.json", JSON.stringify(data, null, 2)); } catch {}
|
|
39
28
|
};
|
|
40
29
|
|
|
@@ -151,16 +140,15 @@ const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
|
|
|
151
140
|
export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime) => {
|
|
152
141
|
pi.on("session_before_compact", (event, ctx) => {
|
|
153
142
|
const { preparation, branchEntries, customInstructions } = event;
|
|
154
|
-
|
|
143
|
+
omRuntime.ensureConfig(ctx.cwd ?? process.cwd());
|
|
155
144
|
|
|
156
145
|
// Always handle explicit /blackhole marker.
|
|
157
146
|
// Otherwise, only handle when user opted in via settings.
|
|
158
147
|
const isPiVcc = customInstructions === PI_VCC_COMPACT_INSTRUCTION;
|
|
159
|
-
if (!isPiVcc && !
|
|
148
|
+
if (!isPiVcc && !omRuntime.config.overrideDefaultCompaction) return;
|
|
160
149
|
|
|
161
150
|
// When noAutoCompact is active, only /blackhole can trigger compaction
|
|
162
|
-
|
|
163
|
-
if (unifiedSettings.noAutoCompact && !isPiVcc) {
|
|
151
|
+
if (omRuntime.config.noAutoCompact && !isPiVcc) {
|
|
164
152
|
return { cancel: true };
|
|
165
153
|
}
|
|
166
154
|
|
|
@@ -192,7 +180,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
192
180
|
}
|
|
193
181
|
const userIndices = liveRoles.reduce<number[]>((acc, r, i) => (r === "user" ? (acc.push(i), acc) : acc), []);
|
|
194
182
|
|
|
195
|
-
dbg(
|
|
183
|
+
dbg(omRuntime.config.debug, {
|
|
196
184
|
cancelled: true,
|
|
197
185
|
reason: ownCut.reason,
|
|
198
186
|
isPiVcc,
|
|
@@ -250,14 +238,12 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
250
238
|
}, 0);
|
|
251
239
|
return sum;
|
|
252
240
|
}, 0);
|
|
253
|
-
|
|
241
|
+
omRuntime.compactionStats = {
|
|
254
242
|
summarized: agentMessages.length,
|
|
255
243
|
kept: keptEntries.length,
|
|
256
244
|
keptTokensEst: Math.round(keptChars / 4),
|
|
257
245
|
};
|
|
258
246
|
|
|
259
|
-
const config = settings;
|
|
260
|
-
|
|
261
247
|
const summary = compile({
|
|
262
248
|
messages,
|
|
263
249
|
previousSummary: preparation.previousSummary,
|
|
@@ -278,7 +264,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
278
264
|
}))
|
|
279
265
|
: [];
|
|
280
266
|
|
|
281
|
-
dbg(config, {
|
|
267
|
+
dbg(omRuntime.config.debug, {
|
|
282
268
|
usedOwnCut: true,
|
|
283
269
|
messagesToSummarize: agentMessages.length,
|
|
284
270
|
messagesPreviewHead: agentMessages.slice(0, 3).map((m: any) => ({ role: m.role, preview: previewContent(m.content) })),
|
|
@@ -300,10 +286,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
300
286
|
previousSummaryUsed: Boolean(preparation.previousSummary),
|
|
301
287
|
};
|
|
302
288
|
|
|
303
|
-
|
|
289
|
+
omRuntime.compactWasPiVcc = isPiVcc;
|
|
304
290
|
|
|
305
291
|
// ── Inject observational-memory content ───────────────────────────
|
|
306
|
-
omRuntime.ensureConfig(ctx.cwd ?? process.cwd());
|
|
307
292
|
let omContent = "";
|
|
308
293
|
let omDetails: Record<string, unknown> | undefined;
|
|
309
294
|
if (omRuntime.config.memory !== false) {
|
|
@@ -330,8 +315,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
330
315
|
// /blackhole path uses its own onComplete callback in the command handler.
|
|
331
316
|
pi.on("session_compact", (event, ctx) => {
|
|
332
317
|
if (!event.fromExtension) return;
|
|
333
|
-
if (
|
|
334
|
-
const stats =
|
|
318
|
+
if (omRuntime.compactWasPiVcc) return; // /blackhole handles its own toast via onComplete
|
|
319
|
+
const stats = omRuntime.compactionStats;
|
|
335
320
|
if (!stats) return;
|
|
336
321
|
setTimeout(() => {
|
|
337
322
|
try {
|
|
@@ -37,10 +37,11 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
37
37
|
const tokens = rawTokensSinceLastCompaction(entries);
|
|
38
38
|
if (tokens < runtime.config.compactAfterTokens) return;
|
|
39
39
|
|
|
40
|
-
// Capture ctx properties synchronously — the
|
|
40
|
+
// Capture ctx properties synchronously — the deferred callback below
|
|
41
41
|
// may outlive the extension ctx (stale after session replacement/reload).
|
|
42
42
|
const hasUI = ctx.hasUI;
|
|
43
43
|
const ui = ctx.ui;
|
|
44
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
44
45
|
|
|
45
46
|
if (hasUI) ui?.notify(
|
|
46
47
|
`Observational memory: compaction threshold reached (~${tokens.toLocaleString()} tokens); triggering compaction`,
|
|
@@ -48,8 +49,19 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
48
49
|
);
|
|
49
50
|
|
|
50
51
|
runtime.compactInFlight = true;
|
|
51
|
-
|
|
52
|
+
queueMicrotask(() => {
|
|
52
53
|
try {
|
|
54
|
+
// Validate session identity — bail if the session was replaced/reloaded.
|
|
55
|
+
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
56
|
+
if (currentSessionId !== sessionId) {
|
|
57
|
+
runtime.compactInFlight = false;
|
|
58
|
+
if (hasUI) ui?.notify(
|
|
59
|
+
"Observational memory: compaction cancelled — session changed before compaction",
|
|
60
|
+
"info",
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
if (!ctx.isIdle()) {
|
|
54
66
|
runtime.compactInFlight = false;
|
|
55
67
|
if (hasUI) ui?.notify(
|
|
@@ -87,6 +99,6 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
87
99
|
const msg = error instanceof Error ? error.message : String(error);
|
|
88
100
|
if (hasUI) ui?.notify(`Observational memory: compact threw: ${msg}`, "error");
|
|
89
101
|
}
|
|
90
|
-
}
|
|
102
|
+
});
|
|
91
103
|
});
|
|
92
104
|
}
|
package/src/om/consolidation.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { type ResolveResult, type Runtime } from "./runtime.js";
|
|
|
18
18
|
import { isRetryableError } from "./cooldown.js";
|
|
19
19
|
import { serializeSourceAddressedBranchEntries } from "./serialize.js";
|
|
20
20
|
import {
|
|
21
|
+
readPendingState,
|
|
21
22
|
savePendingObservation,
|
|
22
23
|
savePendingReflection,
|
|
23
24
|
savePendingDropped,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
buildObservationsRecordedData,
|
|
34
35
|
buildReflectionsRecordedData,
|
|
35
36
|
earlierCoverageMarkerId,
|
|
37
|
+
entryIndexForId,
|
|
36
38
|
foldLedger,
|
|
37
39
|
findLastCompactionIndex,
|
|
38
40
|
fullProjection,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
latestCoverageMarkerId,
|
|
42
44
|
observationsCreatedAfterIndex,
|
|
43
45
|
observationToSummaryLine,
|
|
46
|
+
rawTokensAfterIndex,
|
|
44
47
|
rawTokensSinceDropCoverage,
|
|
45
48
|
rawTokensSinceObservationCoverage,
|
|
46
49
|
rawTokensSinceReflectionCoverage,
|
|
@@ -139,8 +142,8 @@ function stageFallbackModels(runtime: Runtime, stage: "observer" | "reflector" |
|
|
|
139
142
|
return runtime.config.dropperFallbackModels ?? [];
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
function stageThinkingLevel(runtime: Runtime, stage: "observer" | "reflector" | "dropper"): ModelThinkingLevel {
|
|
143
|
-
const stageModel = stageModelConfig(runtime, stage);
|
|
145
|
+
function stageThinkingLevel(runtime: Runtime, stage: "observer" | "reflector" | "dropper", modelConfig?: ConfiguredModel): ModelThinkingLevel {
|
|
146
|
+
const stageModel = modelConfig ?? stageModelConfig(runtime, stage);
|
|
144
147
|
return stageModel?.thinking ?? runtime.config.model?.thinking ?? "low";
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -284,12 +287,23 @@ async function runObserverStage(
|
|
|
284
287
|
const resolved = await resolveModel("observer");
|
|
285
288
|
if (!resolved) return "abort";
|
|
286
289
|
|
|
290
|
+
// Adjust accumulated for pending coverage in noAutoCompact mode
|
|
291
|
+
let effectiveTokens = tokens;
|
|
292
|
+
if (runtime.config.noAutoCompact) {
|
|
293
|
+
const pending = readPendingState(sessionId);
|
|
294
|
+
if (pending.observation?.coversUpToId) {
|
|
295
|
+
const idx = entryIndexForId(entries, pending.observation.coversUpToId);
|
|
296
|
+
if (idx >= 0) effectiveTokens = rawTokensAfterIndex(entries, idx);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
287
299
|
if (ctx.hasUI) ctx.ui?.notify(
|
|
288
|
-
`Observational memory: observer running on ~${chunkTokens.toLocaleString()}-token chunk (of ${
|
|
300
|
+
`Observational memory: observer running on ~${chunkTokens.toLocaleString()}-token chunk (of ${effectiveTokens.toLocaleString()} accumulated)`,
|
|
289
301
|
"info",
|
|
290
302
|
);
|
|
291
303
|
debugLog("observer.start", { tokens, coversUpToId, sourceEntryIds, sourceEntryCount: sourceEntryIds.length, priorReflections: priorReflections.length, priorObservations: priorObservations.length });
|
|
292
304
|
|
|
305
|
+
// Resolve thinking level for the specific model (fallbacks may have their own thinking config)
|
|
306
|
+
const stageModelForThinking = runtime.findCandidateConfig(resolved.model, { model: ctx.model, modelRegistry: ctx.modelRegistry, hasUI: ctx.hasUI, ui: ctx.ui, stageModel: stageModelConfig(runtime, "observer"), stageFallbacks: stageFallbackModels(runtime, "observer") });
|
|
293
307
|
try {
|
|
294
308
|
const result = await runObserver({
|
|
295
309
|
model: resolved.model as any,
|
|
@@ -300,7 +314,7 @@ async function runObserverStage(
|
|
|
300
314
|
chunk,
|
|
301
315
|
allowedSourceEntryIds: sourceEntryIds,
|
|
302
316
|
maxTurns: runtime.config.agentMaxTurns,
|
|
303
|
-
thinkingLevel: stageThinkingLevel(runtime, "observer"),
|
|
317
|
+
thinkingLevel: stageThinkingLevel(runtime, "observer", stageModelForThinking),
|
|
304
318
|
});
|
|
305
319
|
|
|
306
320
|
if (result.observations && result.observations.length > 0) {
|
|
@@ -386,8 +400,19 @@ async function runReflectorStage(
|
|
|
386
400
|
);
|
|
387
401
|
const summaryBudget = Math.floor(runtime.config.reflectorInputMaxTokens * 0.15) * 2;
|
|
388
402
|
const reflectorInputTokens = Math.min(newItemsTokens + summaryBudget, runtime.config.reflectorInputMaxTokens);
|
|
389
|
-
|
|
403
|
+
// Adjust accumulated for pending coverage in noAutoCompact mode
|
|
404
|
+
let effectiveReflectionTokens = reflectionTokens;
|
|
405
|
+
if (runtime.config.noAutoCompact) {
|
|
406
|
+
const pending = readPendingState(sessionId);
|
|
407
|
+
if (pending.reflection?.coversUpToId) {
|
|
408
|
+
const idx = entryIndexForId(entries, pending.reflection.coversUpToId);
|
|
409
|
+
if (idx >= 0) effectiveReflectionTokens = rawTokensAfterIndex(entries, idx);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (ctx.hasUI) ctx.ui?.notify(`Observational memory: reflector running (~${effectiveReflectionTokens.toLocaleString()} tokens accumulated, ~${reflectorInputTokens.toLocaleString()}-token input)`, "info");
|
|
390
413
|
|
|
414
|
+
// Resolve thinking level for the specific model (fallbacks may have their own thinking config)
|
|
415
|
+
const stageModelForThinking = runtime.findCandidateConfig(resolved.model, { model: ctx.model, modelRegistry: ctx.modelRegistry, hasUI: ctx.hasUI, ui: ctx.ui, stageModel: stageModelConfig(runtime, "reflector"), stageFallbacks: stageFallbackModels(runtime, "reflector") });
|
|
391
416
|
try {
|
|
392
417
|
// Existing memory summaries for context (capped)
|
|
393
418
|
const existingReflectionsSummary = buildExistingReflectionsSummary(
|
|
@@ -408,7 +433,7 @@ async function runReflectorStage(
|
|
|
408
433
|
existingReflectionsSummary: existingReflectionsSummary || undefined,
|
|
409
434
|
existingObservationsSummary: existingObservationsSummary || undefined,
|
|
410
435
|
maxTurns: runtime.config.agentMaxTurns,
|
|
411
|
-
thinkingLevel: stageThinkingLevel(runtime, "reflector"),
|
|
436
|
+
thinkingLevel: stageThinkingLevel(runtime, "reflector", stageModelForThinking),
|
|
412
437
|
});
|
|
413
438
|
|
|
414
439
|
if (!reflections || reflections.length === 0) return { outcome: "continue", sameRunReflections: [] };
|
|
@@ -468,7 +493,16 @@ async function runDropperStage(
|
|
|
468
493
|
);
|
|
469
494
|
const dropperSummaryBudget = Math.floor(runtime.config.dropperInputMaxTokens * 0.2);
|
|
470
495
|
const dropperInputTokens = Math.min(dropperNewObsTokens + dropperSummaryBudget, runtime.config.dropperInputMaxTokens);
|
|
471
|
-
|
|
496
|
+
// Adjust accumulated for pending coverage in noAutoCompact mode
|
|
497
|
+
let effectiveDropTokens = dropTokens;
|
|
498
|
+
if (runtime.config.noAutoCompact) {
|
|
499
|
+
const pending = readPendingState(sessionId);
|
|
500
|
+
if (pending.dropped?.coversUpToId) {
|
|
501
|
+
const idx = entryIndexForId(entries, pending.dropped.coversUpToId);
|
|
502
|
+
if (idx >= 0) effectiveDropTokens = rawTokensAfterIndex(entries, idx);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (ctx.hasUI) ctx.ui?.notify(`Observational memory: dropper running (~${effectiveDropTokens.toLocaleString()} tokens accumulated, ~${dropperInputTokens.toLocaleString()}-token input)`, "info");
|
|
472
506
|
|
|
473
507
|
try {
|
|
474
508
|
// Existing active observations summary for context (capped)
|
|
@@ -478,6 +512,8 @@ async function runDropperStage(
|
|
|
478
512
|
);
|
|
479
513
|
const reflectionsForDropper = mergeReflections(folded.reflections, sameRunReflections);
|
|
480
514
|
|
|
515
|
+
// Resolve thinking level for the specific model (fallbacks may have their own thinking config)
|
|
516
|
+
const stageModelForThinking = runtime.findCandidateConfig(resolved.model, { model: ctx.model, modelRegistry: ctx.modelRegistry, hasUI: ctx.hasUI, ui: ctx.ui, stageModel: stageModelConfig(runtime, "dropper"), stageFallbacks: stageFallbackModels(runtime, "dropper") });
|
|
481
517
|
const droppedIds = await runDropper({
|
|
482
518
|
model: resolved.model as any,
|
|
483
519
|
apiKey: resolved.apiKey,
|
|
@@ -487,7 +523,7 @@ async function runDropperStage(
|
|
|
487
523
|
existingObservationsSummary: existingObservationsSummary || undefined,
|
|
488
524
|
budgetTokens: runtime.config.observationsPoolMaxTokens,
|
|
489
525
|
maxTurns: runtime.config.agentMaxTurns,
|
|
490
|
-
thinkingLevel: stageThinkingLevel(runtime, "dropper"),
|
|
526
|
+
thinkingLevel: stageThinkingLevel(runtime, "dropper", stageModelForThinking),
|
|
491
527
|
});
|
|
492
528
|
const latestReflectionCoverageId = latestCoverageMarkerId(entries, OM_REFLECTIONS_RECORDED);
|
|
493
529
|
const effectiveReflectionCoverageId = sameRunReflectionCoverageId ?? latestReflectionCoverageId;
|
|
@@ -157,7 +157,10 @@ export function fullProjection(entries: Entry[], upToEntryId?: string): Projecti
|
|
|
157
157
|
export function visibleProjection(entries: Entry[], upToEntryId?: string): Projection {
|
|
158
158
|
if (!upToEntryId) {
|
|
159
159
|
const details = latestV3CompactionDetails(entries);
|
|
160
|
-
|
|
160
|
+
if (details) return projectionFromMemoryDetails(details);
|
|
161
|
+
// No compaction has run yet — show everything so the user sees
|
|
162
|
+
// recorded data until first /blackhole creates a proper snapshot.
|
|
163
|
+
return fullProjection(entries);
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
return buildCompactionProjection(entries, upToEntryId, { observationsPoolMaxTokens: Number.POSITIVE_INFINITY });
|
package/src/om/ledger/recall.ts
CHANGED
|
@@ -188,7 +188,7 @@ export function recallMemorySources(entries: Entry[], memoryId: string): RecallR
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
const recalledByKey = new Map<string, RecalledObservation>();
|
|
191
|
-
const
|
|
191
|
+
const rawMissingSupportingObservationIds: string[] = [];
|
|
192
192
|
|
|
193
193
|
function addObservation(indexed: IndexedObservation): void {
|
|
194
194
|
const key = `${indexed.entryId}:${indexed.recordIndex}`;
|
|
@@ -204,7 +204,7 @@ export function recallMemorySources(entries: Entry[], memoryId: string): RecallR
|
|
|
204
204
|
for (const observationId of uniqueStrings(reflection.supportingObservationIds)) {
|
|
205
205
|
const indexed = observationsById.get(observationId);
|
|
206
206
|
if (!indexed) {
|
|
207
|
-
|
|
207
|
+
rawMissingSupportingObservationIds.push(observationId);
|
|
208
208
|
continue;
|
|
209
209
|
}
|
|
210
210
|
addObservation(indexed);
|
|
@@ -220,7 +220,7 @@ export function recallMemorySources(entries: Entry[], memoryId: string): RecallR
|
|
|
220
220
|
const sourceEntries = uniqueById(recalledObservations.flatMap((match) => match.sourceEntries));
|
|
221
221
|
const missingSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.missingSourceEntryIds));
|
|
222
222
|
const nonSourceEntryIds = uniqueStrings(recalledObservations.flatMap((match) => match.nonSourceEntryIds));
|
|
223
|
-
const uniqueMissingSupportingObservationIds = uniqueStrings(
|
|
223
|
+
const uniqueMissingSupportingObservationIds = uniqueStrings(rawMissingSupportingObservationIds);
|
|
224
224
|
const matchCount = directObservationMatches.length + reflectionMatches.length;
|
|
225
225
|
|
|
226
226
|
return {
|
package/src/om/reverse-recall.ts
CHANGED
|
@@ -58,11 +58,10 @@ export function findReflectionsForEntryIds(
|
|
|
58
58
|
targetEntryIds: string[],
|
|
59
59
|
): RelatedReflection[] {
|
|
60
60
|
if (targetEntryIds.length === 0) return [];
|
|
61
|
-
const { reflections } = indexLedger(entries);
|
|
61
|
+
const { reflections, observations } = indexLedger(entries);
|
|
62
62
|
// Reflections don't directly reference entry IDs — they reference observation IDs.
|
|
63
63
|
// So we only match indirectly: first find observations for these entry IDs,
|
|
64
64
|
// then find reflections that support those observations.
|
|
65
|
-
const { observations } = indexLedger(entries);
|
|
66
65
|
const targetSet = new Set(targetEntryIds);
|
|
67
66
|
const matchingObsIds = new Set<string>();
|
|
68
67
|
for (const indexed of observations) {
|
package/src/om/runtime.ts
CHANGED
|
@@ -53,6 +53,10 @@ export class Runtime {
|
|
|
53
53
|
lastDropperError: string | undefined;
|
|
54
54
|
/** Epoch ms of the last failed consolidation run (any stage). */
|
|
55
55
|
lastConsolidationErrorAt: number | undefined;
|
|
56
|
+
/** Stats from the most recent compaction run (session-scoped via handler closure). */
|
|
57
|
+
compactionStats: { summarized: number; kept: number; keptTokensEst: number } | null = null;
|
|
58
|
+
/** Whether the most recent compaction was triggered by /blackhole (vs auto-compact). */
|
|
59
|
+
compactWasPiVcc = false;
|
|
56
60
|
|
|
57
61
|
ensureConfig(cwd: string): void {
|
|
58
62
|
if (this.configLoaded) return;
|
package/src/core/report.ts
DELETED
|
@@ -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
|
-
}
|