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.
- package/README.md +7 -2
- package/example-config.json +14 -3
- package/package.json +12 -7
- package/src/commands/memory.ts +36 -8
- package/src/commands/pi-vcc.ts +23 -8
- 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 +11 -5
- package/src/hooks/before-compact.ts +11 -26
- package/src/om/compaction-trigger.ts +15 -3
- package/src/om/consolidation.ts +250 -37
- package/src/om/ledger/projection.ts +4 -1
- package/src/om/ledger/recall.ts +3 -3
- package/src/om/pending.ts +34 -2
- 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
|
@@ -131,6 +131,7 @@ The tradeoff is simplicity vs cleanliness:
|
|
|
131
131
|
|---|---|---|
|
|
132
132
|
| Workers run? | Yes | Yes |
|
|
133
133
|
| Observations go to | Conversation markers (invisible in TUI) | Disk (`pending.json`) |
|
|
134
|
+
| Observations accumulate across runs | Branch markers (replaced each cycle) | Pending batches — `/memory` shows pending counts |
|
|
134
135
|
| Auto-compact on `agent_end` | Yes | No |
|
|
135
136
|
| `/blackhole` | Optional — use it whenever you want | Required to flush + compact |
|
|
136
137
|
| Conversation history | OM marker entries between turns (they exist but do not clutter the display) | Clean — nothing between turns |
|
|
@@ -170,7 +171,7 @@ Set `memory: false` or run `/blackhole om-off` for pure pi-vcc compaction — no
|
|
|
170
171
|
|
|
171
172
|
## Configuration
|
|
172
173
|
|
|
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.
|
|
174
|
+
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
175
|
|
|
175
176
|
Quick start with custom models:
|
|
176
177
|
|
|
@@ -198,6 +199,7 @@ Quick start with custom models:
|
|
|
198
199
|
| `reflectAfterTokens` | `20000` | Token cadence for reflector and dropper |
|
|
199
200
|
| `compactAfterTokens` | `81000` | Auto-compaction threshold |
|
|
200
201
|
| `observerChunkMaxTokens` | `40000` | Max observer input per run (newest-first) |
|
|
202
|
+
| `observerPreambleMaxTokens` | `0` (auto) | Max preamble tokens in observer prompt for `noAutoCompact` mode (auto = 30% of chunk) |
|
|
201
203
|
| `observationsPoolMaxTokens` | `20000` | Max active observation pool before dropper prunes |
|
|
202
204
|
| `reflectorInputMaxTokens` | `80000` | Max reflector input budget |
|
|
203
205
|
| `dropperInputMaxTokens` | `80000` | Max dropper input budget |
|
|
@@ -220,6 +222,7 @@ Paste the appropriate block into your config to match your model's context size.
|
|
|
220
222
|
"reflectAfterTokens": 10000,
|
|
221
223
|
"compactAfterTokens": 30000,
|
|
222
224
|
"observerChunkMaxTokens": 15000,
|
|
225
|
+
"observerPreambleMaxTokens": 0,
|
|
223
226
|
"observationsPoolMaxTokens": 8000,
|
|
224
227
|
"reflectorInputMaxTokens": 30000,
|
|
225
228
|
"dropperInputMaxTokens": 30000
|
|
@@ -236,6 +239,7 @@ Our built-in defaults already target this tier. If you reset your config, these
|
|
|
236
239
|
"reflectAfterTokens": 20000,
|
|
237
240
|
"compactAfterTokens": 81000,
|
|
238
241
|
"observerChunkMaxTokens": 40000,
|
|
242
|
+
"observerPreambleMaxTokens": 0,
|
|
239
243
|
"observationsPoolMaxTokens": 20000,
|
|
240
244
|
"reflectorInputMaxTokens": 80000,
|
|
241
245
|
"dropperInputMaxTokens": 80000
|
|
@@ -250,6 +254,7 @@ Our built-in defaults already target this tier. If you reset your config, these
|
|
|
250
254
|
"reflectAfterTokens": 40000,
|
|
251
255
|
"compactAfterTokens": 180000,
|
|
252
256
|
"observerChunkMaxTokens": 80000,
|
|
257
|
+
"observerPreambleMaxTokens": 0,
|
|
253
258
|
"observationsPoolMaxTokens": 40000,
|
|
254
259
|
"reflectorInputMaxTokens": 160000,
|
|
255
260
|
"dropperInputMaxTokens": 160000
|
|
@@ -354,7 +359,7 @@ pnpm add pi-blackhole
|
|
|
354
359
|
|
|
355
360
|
```bash
|
|
356
361
|
pi uninstall git:github.com/k0valik/pi-blackhole
|
|
357
|
-
rm -rf ~/.pi/agent/pi-blackhole
|
|
362
|
+
rm -rf ~/.pi/agent/pi-blackhole
|
|
358
363
|
```
|
|
359
364
|
|
|
360
365
|
---
|
package/example-config.json
CHANGED
|
@@ -86,13 +86,14 @@
|
|
|
86
86
|
}
|
|
87
87
|
],
|
|
88
88
|
|
|
89
|
-
"observeAfterTokens":
|
|
90
|
-
"reflectAfterTokens":
|
|
89
|
+
"observeAfterTokens": 15000,
|
|
90
|
+
"reflectAfterTokens": 25000,
|
|
91
91
|
"compactAfterTokens": 81000,
|
|
92
92
|
"observationsPoolMaxTokens": 20000,
|
|
93
93
|
"reflectorInputMaxTokens": 80000,
|
|
94
94
|
"dropperInputMaxTokens": 80000,
|
|
95
95
|
"observerChunkMaxTokens": 40000,
|
|
96
|
+
"observerPreambleMaxTokens": 0,
|
|
96
97
|
"agentMaxTurns": 16,
|
|
97
98
|
|
|
98
99
|
"passive": false,
|
|
@@ -110,6 +111,16 @@
|
|
|
110
111
|
"After all candidates exhausted, the stage aborts. 30s retry gate prevents spam.",
|
|
111
112
|
"Cooldowns persist in ~/.pi/agent/pi-blackhole/pi-blackhole-cooldown.json (survives pi restarts).",
|
|
112
113
|
"Valid thinking values: off, minimal, low, medium, high, xhigh.",
|
|
113
|
-
"Env override for passive mode: PI_BLACKHOLE_PASSIVE=true."
|
|
114
|
+
"Env override for passive mode: PI_BLACKHOLE_PASSIVE=true.",
|
|
115
|
+
"",
|
|
116
|
+
"noAutoCompact mode accumulates observationBatches/reflectionBatches in",
|
|
117
|
+
"pending.json across pipeline runs. The observer preamble (CURRENT OBSERVATIONS)",
|
|
118
|
+
"is capped to observerPreambleMaxTokens (default 0 = auto 30% of observerChunkMaxTokens).",
|
|
119
|
+
"This is a soft safety valve — it only trims when accumulated observations exceed",
|
|
120
|
+
"the budget. High-relevance observations are always kept. Reflections are never trimmed.",
|
|
121
|
+
"",
|
|
122
|
+
"agentMaxTurns controls how many agent-loop turns each worker gets per chunk.",
|
|
123
|
+
"Lower-context models can reduce this (e.g., 8) so the observer doesn't waste",
|
|
124
|
+
"turns trying to extract more than it can reliably handle per run."
|
|
114
125
|
]
|
|
115
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-blackhole",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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 ──",
|
|
@@ -144,11 +164,12 @@ export function registerMemoryCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
144
164
|
observationLine,
|
|
145
165
|
reflectionLine,
|
|
146
166
|
"",
|
|
147
|
-
"──
|
|
148
|
-
|
|
149
|
-
`
|
|
150
|
-
`
|
|
151
|
-
`
|
|
167
|
+
"── Pipeline ──",
|
|
168
|
+
"Transcript accumulated since last run. Triggers when exceeding threshold.",
|
|
169
|
+
`Observer: ~${obsProgress.toLocaleString()} tokens (triggers at ${runtime.config.observeAfterTokens.toLocaleString()})`,
|
|
170
|
+
`Reflector: ~${reflectionProgress.toLocaleString()} tokens (triggers at ${runtime.config.reflectAfterTokens.toLocaleString()})`,
|
|
171
|
+
`Dropper: ~${dropProgress.toLocaleString()} tokens (triggers at ${runtime.config.reflectAfterTokens.toLocaleString()})`,
|
|
172
|
+
`Compaction: ~${compactionProgress.toLocaleString()} tokens` + (runtime.config.noAutoCompact ? " [auto-disabled]" : ` (triggers at ${runtime.config.compactAfterTokens.toLocaleString()})`),
|
|
152
173
|
`Obs pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
|
|
153
174
|
`Reflect pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
|
|
154
175
|
];
|
|
@@ -164,6 +185,13 @@ export function registerMemoryCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
164
185
|
if (hasObs) lines.push("Observation: waiting in pending.json");
|
|
165
186
|
if (hasRef) lines.push("Reflection: waiting in pending.json");
|
|
166
187
|
if (hasDrop) lines.push("Dropper: waiting in pending.json");
|
|
188
|
+
const preambleCap = runtime.config.observerPreambleMaxTokens > 0
|
|
189
|
+
? runtime.config.observerPreambleMaxTokens
|
|
190
|
+
: Math.round(runtime.config.observerChunkMaxTokens * 0.3);
|
|
191
|
+
const pctNote = runtime.config.observerPreambleMaxTokens > 0
|
|
192
|
+
? ""
|
|
193
|
+
: ` (30% of ${runtime.config.observerChunkMaxTokens.toLocaleString()} chunk)`;
|
|
194
|
+
lines.push(`Preamble cap: ${preambleCap.toLocaleString()} tokens for observations${pctNote}`);
|
|
167
195
|
lines.push("Run /blackhole to flush and compact.");
|
|
168
196
|
}
|
|
169
197
|
}
|
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 {
|
|
@@ -55,14 +55,29 @@ export const registerPiVccCommand = (pi: ExtensionAPI, runtime: Runtime) => {
|
|
|
55
55
|
// before compacting so the summary includes accumulated memory.
|
|
56
56
|
if (runtime.config.noAutoCompact && hasPendingData(sessionId)) {
|
|
57
57
|
const pending = readPendingState(sessionId);
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// Write all accumulated observation batches (or latest single batch
|
|
59
|
+
// as fallback for legacy pending.json without batch arrays).
|
|
60
|
+
const obsBatches = pending.observationBatches?.length
|
|
61
|
+
? pending.observationBatches
|
|
62
|
+
: (pending.observation ? [pending.observation] : []);
|
|
63
|
+
for (const batch of obsBatches) {
|
|
64
|
+
pi.appendEntry(OM_OBSERVATIONS_RECORDED, batch.data);
|
|
60
65
|
}
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
// Write all accumulated reflection batches (or latest single batch
|
|
67
|
+
// as fallback for legacy pending.json without batch arrays).
|
|
68
|
+
const reflBatches = pending.reflectionBatches?.length
|
|
69
|
+
? pending.reflectionBatches
|
|
70
|
+
: (pending.reflection ? [pending.reflection] : []);
|
|
71
|
+
for (const batch of reflBatches) {
|
|
72
|
+
pi.appendEntry(OM_REFLECTIONS_RECORDED, batch.data);
|
|
63
73
|
}
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
// Write all accumulated dropper batches (or latest single batch
|
|
75
|
+
// as fallback for legacy pending.json without batch arrays).
|
|
76
|
+
const dropBatches = pending.droppedBatches?.length
|
|
77
|
+
? pending.droppedBatches
|
|
78
|
+
: (pending.dropped ? [pending.dropped] : []);
|
|
79
|
+
for (const batch of dropBatches) {
|
|
80
|
+
pi.appendEntry(OM_OBSERVATIONS_DROPPED, batch.data);
|
|
66
81
|
}
|
|
67
82
|
clearPendingState(sessionId);
|
|
68
83
|
ctx.ui.notify("Observational memory: pending entries flushed", "info");
|
|
@@ -71,7 +86,7 @@ export const registerPiVccCommand = (pi: ExtensionAPI, runtime: Runtime) => {
|
|
|
71
86
|
ctx.compact({
|
|
72
87
|
customInstructions: PI_VCC_COMPACT_INSTRUCTION,
|
|
73
88
|
onComplete: () => {
|
|
74
|
-
const stats =
|
|
89
|
+
const stats = runtime.compactionStats;
|
|
75
90
|
if (stats) {
|
|
76
91
|
ctx.ui.notify(
|
|
77
92
|
`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
|
+
|
|
@@ -50,6 +50,11 @@ export interface UnifiedConfig {
|
|
|
50
50
|
dropperInputMaxTokens: number;
|
|
51
51
|
/** Max source entries tokens sent to observer per chunk. */
|
|
52
52
|
observerChunkMaxTokens: number;
|
|
53
|
+
/** Max preamble tokens (CURRENT REFLECTIONS / OBSERVATIONS) in the observer prompt.
|
|
54
|
+
* Default 0 means auto-compute from observerChunkMaxTokens (30%). Only applied in
|
|
55
|
+
* noAutoCompact mode where accumulated batch history can grow unbounded.
|
|
56
|
+
* Set to an explicit value to override the auto-computed budget. */
|
|
57
|
+
observerPreambleMaxTokens: number;
|
|
53
58
|
/** Shared turn cap for background memory agents. */
|
|
54
59
|
agentMaxTurns: number;
|
|
55
60
|
|
|
@@ -87,13 +92,14 @@ export const DEFAULTS: UnifiedConfig = {
|
|
|
87
92
|
overrideDefaultCompaction: false,
|
|
88
93
|
debug: false,
|
|
89
94
|
|
|
90
|
-
observeAfterTokens:
|
|
91
|
-
reflectAfterTokens:
|
|
95
|
+
observeAfterTokens: 15_000,
|
|
96
|
+
reflectAfterTokens: 25_000,
|
|
92
97
|
compactAfterTokens: 81_000,
|
|
93
98
|
observationsPoolMaxTokens: 20_000,
|
|
94
99
|
reflectorInputMaxTokens: 80_000,
|
|
95
100
|
dropperInputMaxTokens: 80_000,
|
|
96
101
|
observerChunkMaxTokens: 40_000,
|
|
102
|
+
observerPreambleMaxTokens: 0,
|
|
97
103
|
agentMaxTurns: 16,
|
|
98
104
|
|
|
99
105
|
noAutoCompact: false,
|
|
@@ -154,7 +160,7 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
|
|
|
154
160
|
if (typeof raw.debugLog === "boolean") c.debugLog = raw.debugLog;
|
|
155
161
|
|
|
156
162
|
// Positive integers
|
|
157
|
-
const numKeys = ["observeAfterTokens", "reflectAfterTokens", "compactAfterTokens", "observationsPoolMaxTokens", "reflectorInputMaxTokens", "dropperInputMaxTokens", "observerChunkMaxTokens", "agentMaxTurns"] as const;
|
|
163
|
+
const numKeys = ["observeAfterTokens", "reflectAfterTokens", "compactAfterTokens", "observationsPoolMaxTokens", "reflectorInputMaxTokens", "dropperInputMaxTokens", "observerChunkMaxTokens", "observerPreambleMaxTokens", "agentMaxTurns"] as const;
|
|
158
164
|
for (const k of numKeys) {
|
|
159
165
|
const v = positiveInt(raw[k]);
|
|
160
166
|
if (v !== undefined) (c as Record<string, unknown>)[k] = v;
|
|
@@ -279,7 +285,7 @@ export function scaffoldConfig(): void {
|
|
|
279
285
|
}
|
|
280
286
|
}
|
|
281
287
|
if (changed) writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
282
|
-
} catch {
|
|
283
|
-
|
|
288
|
+
} catch (e) {
|
|
289
|
+
console.error("blackhole: config scaffold failed", e);
|
|
284
290
|
}
|
|
285
291
|
}
|
|
@@ -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
|
}
|