pi-blackhole 0.3.0 → 0.3.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 +265 -121
- package/example-config.json +6 -2
- package/package.json +1 -1
- package/src/hooks/before-compact.ts +27 -1
- package/src/om/compaction-trigger.ts +67 -9
- package/src/om/debug-log.ts +5 -4
package/README.md
CHANGED
|
@@ -1,91 +1,158 @@
|
|
|
1
1
|
# pi-blackhole
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> **For blackhole to handle all compaction automatically**, set `"overrideDefaultCompaction": true` in your config at
|
|
5
|
+
> `~/.pi/agent/pi-blackhole/pi-blackhole-config.json`. Without this, blackhole stays out of Pi's compaction by default
|
|
6
|
+
> and only activates via the `/blackhole` command.
|
|
7
|
+
>
|
|
8
|
+
> See [`CONFIG.md`](CONFIG.md) for the full reference.
|
|
9
|
+
|
|
10
|
+
**Algorithmic compaction + session-aware observational memory for [Pi](https://github.com/badlogic/pi-mono) — in one unified extension.**
|
|
11
|
+
|
|
12
|
+
Blackhole merges the best ideas from [pi-vcc](https://github.com/sting8k/pi-vcc) and [pi-observational-memory](https://github.com/elpapi42/pi-observational-memory) into something that's become its own beast entirely. Deterministic compaction that costs nothing. A memory layer that survives compactions. Per-worker model fallback chains with persisted cooldowns. Manual flush mode. All configured from one JSON file.
|
|
13
|
+
|
|
14
|
+
> **Why this exists:** I liked both extensions but they fought each other — OM hooked into Pi's default compaction and blocked vcc from working. So I merged them, made them share a single hook and output, and added everything both were missing: fallback chains, cooldowns, a memory toggle, and a manual mode for people who want to control when context gets compressed.
|
|
15
|
+
>
|
|
16
|
+
> The codebase has since diverged heavily from both upstreams, but tries to keep up-to-date with any fixes from them..
|
|
17
|
+
|
|
18
|
+
> [!IMPORTANT]
|
|
19
|
+
> **Auto-compaction behavior & configuration**
|
|
20
|
+
>
|
|
21
|
+
> Blackhole's auto-compaction **only activates when explicitly opted in**. By default (`overrideDefaultCompaction: false`), blackhole stays out of Pi's compaction — Pi handles its own automatically, and blackhole's `/blackhole` command still works manually.
|
|
22
|
+
>
|
|
23
|
+
> Configure via your Pi agent's `config.json`:
|
|
24
|
+
>
|
|
25
|
+
> | What you want | `overrideDefaultCompaction` | `noAutoCompact` |
|
|
26
|
+
> |---|---|---|
|
|
27
|
+
> | Blackhole auto-compacts at your threshold | `true` | `false` |
|
|
28
|
+
> | Pi handles auto-compaction (default) | `false` | `false` |
|
|
29
|
+
> | Manual only — use `/blackhole` | any | `true` |
|
|
30
|
+
>
|
|
31
|
+
> See [`CONFIG.md`](CONFIG.md) for the full reference.
|
|
32
|
+
|
|
33
|
+
📖 See [`CHANGELOG.md`](CHANGELOG.md) for release history.
|
|
34
|
+
⚙️ See [`CONFIG.md`](CONFIG.md) for the full configuration reference.
|
|
4
35
|
|
|
5
|
-
|
|
36
|
+
---
|
|
6
37
|
|
|
7
|
-
|
|
8
|
-
> Please also see the [`CHANGELOG.md`](CHANGELOG.md)
|
|
38
|
+
## Quick start
|
|
9
39
|
|
|
10
|
-
|
|
40
|
+
```bash
|
|
41
|
+
# Install from npm (recommended)
|
|
42
|
+
pi install npm:pi-blackhole
|
|
11
43
|
|
|
12
|
-
|
|
44
|
+
# Or directly from GitHub
|
|
45
|
+
pi install git:github.com/k0valik/pi-blackhole
|
|
46
|
+
```
|
|
13
47
|
|
|
14
|
-
|
|
48
|
+
If you have standalone pi-vcc or pi-observational-memory installed, remove them first — they conflict and will prevent blackhole from working. You don't loose any features from either extension:
|
|
15
49
|
|
|
50
|
+
```bash
|
|
51
|
+
pi uninstall npm / git:https://github.com/sting8k/pi-vcc
|
|
52
|
+
pi uninstall npm / git:https://github.com/elpapi42/pi-observational-memory
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then `/reload` or restart Pi.
|
|
16
56
|
|
|
17
|
-
|
|
57
|
+
### Automated setup
|
|
18
58
|
|
|
19
|
-
|
|
59
|
+
Pass [`llms.txt`](llms.txt) to your agent and it will walk you through configuration step by step — no need to read all the docs.
|
|
60
|
+
|
|
61
|
+
### Lockstep with upstreams
|
|
62
|
+
|
|
63
|
+
pi-blackhole tracks both upstream repositories via a [lockstep audit system](.pi/skills/lockstep/SKILL.md). Every new commit from [pi-vcc](https://github.com/sting8k/pi-vcc) and [pi-observational-memory](https://github.com/elpapi42/pi-observational-memory) is classified as safe-to-port, modified (needs review), rewritten (skip), or orphan (needs mapping). Bugfixes and compatible improvements get ported; intentional divergences stay. Nothing is blindly merged — every ported change is reviewed per-commit with human approval. See `.pi/skills/lockstep/` for the full workflow.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Demo
|
|
68
|
+
|
|
69
|
+
`/blackhole` collapses ~94k tokens of conversation into a ~7.8k structured summary (YMMV based on your settings). `/blackhole-memory` shows pipeline status. `/blackhole-recall` searches history the agent can also reach via its `recall` tool.
|
|
20
70
|
|
|
21
71
|
https://github.com/user-attachments/assets/ab7c5787-1bbd-466f-a231-0818e68e9f39
|
|
22
72
|
|
|
73
|
+
|
|
23
74
|
---
|
|
24
75
|
|
|
25
|
-
## The problem
|
|
76
|
+
## The problem it solves
|
|
26
77
|
|
|
27
|
-
Long
|
|
78
|
+
Long engineering sessions degrade. Pi's native compaction calls an LLM to write free-form prose summaries — then compacts those summaries, then compacts *those* summaries. After enough cycles, load-bearing details vanish: why a decision was made, what approaches were already rejected, what the user clarified earlier.
|
|
28
79
|
|
|
29
80
|
The session is still alive. The agent is no longer carrying the real context.
|
|
30
81
|
|
|
31
|
-
The two upstream projects solve
|
|
82
|
+
The two upstream projects each solve one half:
|
|
32
83
|
|
|
33
|
-
- **pi-vcc** replaces Pi's LLM-based compaction with a deterministic, zero-cost algorithmic summary. Fast, reproducible, no hallucination risk. But
|
|
34
|
-
- **pi-observational-memory** captures observations and reflections in a session ledger that survives compactions. But its compaction path still calls an LLM.
|
|
84
|
+
- **pi-vcc** replaces Pi's LLM-based compaction with a deterministic, zero-cost algorithmic summary. Fast, reproducible, no hallucination risk. But repeated compactions still erode detail — it's still a summary.
|
|
85
|
+
- **pi-observational-memory** captures timestamped observations and durable reflections in a session ledger that survives across compactions. But its compaction path still calls an LLM — costing money and risking drift on every compact.
|
|
35
86
|
|
|
36
87
|
**pi-blackhole** puts vcc in the compaction slot and OM in the memory layer, where each does what it's designed for.
|
|
37
88
|
|
|
89
|
+
|
|
38
90
|
---
|
|
39
91
|
|
|
40
|
-
## Feature
|
|
92
|
+
## Feature comparison
|
|
41
93
|
|
|
42
94
|
| | pi-blackhole | pi-vcc | pi-obs-memory | Pi default |
|
|
43
95
|
|---|---|---|---|---|
|
|
44
|
-
| Algorithmic compaction (no LLM) | ✓ | ✓ | — | — |
|
|
96
|
+
| Algorithmic compaction (no LLM cost) | ✓ | ✓ | — | — |
|
|
45
97
|
| Deterministic output | ✓ | ✓ | — | — |
|
|
46
98
|
| Structured summary sections | ✓ | ✓ | — | — |
|
|
47
99
|
| Observations + reflections | ✓ | — | ✓ | — |
|
|
48
100
|
| Context survives across compactions | ✓ | — | ✓ | — |
|
|
49
|
-
| Background workers | ✓ | — | ✓ | — |
|
|
101
|
+
| Background memory workers | ✓ | — | ✓ | — |
|
|
50
102
|
| Searchable history after compaction | ✓ | ✓ | partial | — |
|
|
51
103
|
| Per-worker model config | ✓ | — | — | — |
|
|
52
104
|
| Fallback model chains + persisted cooldowns | ✓ | — | — | — |
|
|
53
105
|
| Manual flush mode (`noAutoCompact`) | ✓ | — | — | — |
|
|
54
106
|
| Memory toggle (`/blackhole om-off`) | ✓ | — | — | — |
|
|
55
107
|
| Unified single-file config | ✓ | — | — | — |
|
|
108
|
+
| Per-session pending state | ✓ | — | — | — |
|
|
109
|
+
|
|
56
110
|
|
|
57
111
|
---
|
|
58
112
|
|
|
59
113
|
## How it works
|
|
60
114
|
|
|
61
|
-
|
|
115
|
+
When you run `/blackhole` (or when auto-compaction fires), two things happen in one shot:
|
|
62
116
|
|
|
63
|
-
|
|
117
|
+
1. **The vcc pipeline** analyzes the transcript tail and produces a structured summary: session goal, file changes, commits, outstanding blockers, user preferences, and a rolling brief transcript.
|
|
118
|
+
2. **Observational memory injection** renders accumulated observations and reflections from the session ledger and appends them below the summary.
|
|
64
119
|
|
|
65
|
-
|
|
66
|
-
|
|
120
|
+
The agent receives a deterministic recap of recent work *plus* durable facts from the full session history — in a single replacement block. No LLM was called for the compaction itself.
|
|
121
|
+
|
|
122
|
+
### The three memory workers
|
|
123
|
+
|
|
124
|
+
Three background workers run automatically during the session (when `memory: true`, which is the default):
|
|
125
|
+
|
|
126
|
+
- **Observer** — reads conversation since the last observation marker and extracts timestamped facts: events, decisions, preferences. Input is capped to `observerChunkMaxTokens` newest-first to prevent context blowup on long sessions. Runs most frequently.
|
|
127
|
+
- **Reflector** — distills new observations into durable reflections: stable facts, patterns, and constraints that survive future compactions. Runs less often.
|
|
67
128
|
- **Dropper** — prunes low-value observations from active memory when the pool exceeds `observationsPoolMaxTokens`, while keeping reflections and other long-term elements safely in the session ledger.
|
|
68
129
|
|
|
69
130
|
```
|
|
70
|
-
[Conversation turn] ──> observeAfterTokens
|
|
131
|
+
[Conversation turn] ──> (accumulated tokens >= observeAfterTokens)
|
|
71
132
|
│
|
|
72
133
|
v
|
|
73
134
|
1. OBSERVER
|
|
74
|
-
(extracts observations)
|
|
135
|
+
(extracts timestamped observations via agent loop)
|
|
75
136
|
│
|
|
76
137
|
v
|
|
77
138
|
2. REFLECTOR
|
|
78
|
-
(
|
|
139
|
+
(synthesizes durable reflections via agent loop)
|
|
79
140
|
│
|
|
80
141
|
v
|
|
81
142
|
3. DROPPER
|
|
82
|
-
(prunes observations, keeps reflections)
|
|
143
|
+
(prunes low-value observations, keeps reflections)
|
|
83
144
|
```
|
|
84
145
|
|
|
85
|
-
|
|
146
|
+
Each worker uses an `agentLoop` with tool-calling capabilities — they don't just make a single LLM call. The observer, for example, can call `record_observations` multiple times per run to work through a chunk incrementally.
|
|
147
|
+
|
|
148
|
+
### Graceful degradation
|
|
149
|
+
|
|
150
|
+
If any stage fails (model error, rate limit, timeout), remaining stages are skipped and the full pipeline retries on the next `agent_start` or `turn_end`. A 30-second retry gate prevents hammering failing APIs. Within each stage, the runtime tries all configured fallback models before giving up — each failed model is cooled down and skipped in subsequent attempts.
|
|
86
151
|
|
|
87
152
|
### What the agent sees after compaction
|
|
88
153
|
|
|
154
|
+
After compaction, the agent sees something like this (sections appear only when relevant — a session with no git commits won't show `[Commits]`):
|
|
155
|
+
|
|
89
156
|
```
|
|
90
157
|
[Session Goal]
|
|
91
158
|
- Fix the authentication bug in login flow
|
|
@@ -115,12 +182,28 @@ Root cause is a missing token refresh...
|
|
|
115
182
|
|
|
116
183
|
---
|
|
117
184
|
|
|
185
|
+
## Reflections
|
|
186
|
+
[c3d4e5f6a1b2] User is building Acme Dashboard on Next.js 15 with Supabase auth.
|
|
187
|
+
|
|
188
|
+
## Observations
|
|
118
189
|
[a1b2c3d4e5f6] 2026-05-23 [high] User decided to switch from REST to GraphQL; motivation was reducing over-fetching.
|
|
119
190
|
[b2c3d4e5f6a1] 2026-05-23 [medium] GraphQL migration completed; user confirmed working.
|
|
120
|
-
|
|
191
|
+
|
|
192
|
+
----
|
|
193
|
+
Use `recall` to search for prior work, decisions, and context from before this summary. Do not redo work already completed.
|
|
194
|
+
|
|
195
|
+
These are condensed memories from earlier in this session.
|
|
196
|
+
|
|
197
|
+
- Reflections: stable, long-lived facts about the user, project, decisions, and constraints.
|
|
198
|
+
- Observations: timestamped events from the conversation history, in chronological order.
|
|
199
|
+
|
|
200
|
+
Treat these as past records. When entries conflict, the most recent observation reflects the latest known state. Work that prior observations describe as completed should not be redone unless the user explicitly asks to revisit it.
|
|
201
|
+
|
|
202
|
+
When exact source context is needed for precision or traceability, use the `recall` tool with the relevant observation or reflection id.
|
|
203
|
+
----
|
|
121
204
|
```
|
|
122
205
|
|
|
123
|
-
|
|
206
|
+
> **Note:** The OM injection format uses `## Reflections` and `## Observations` Markdown headers with a preamble and footer. Each observation and reflection has a 12-char hex identifier you can use with the `recall` tool to recover source evidence.
|
|
124
207
|
|
|
125
208
|
---
|
|
126
209
|
|
|
@@ -129,31 +212,37 @@ Sections appear only when relevant — a session with no git commits won't have
|
|
|
129
212
|
Two modes, one shared goal: keep your agent's context sharp without manual housekeeping.
|
|
130
213
|
|
|
131
214
|
- **Auto mode (default):** install and forget. Workers run, observations are appended as invisible conversation markers, compaction fires automatically when tokens exceed threshold.
|
|
132
|
-
- **Manual mode (`noAutoCompact: true` — the maintainer's
|
|
215
|
+
- **Manual mode (`noAutoCompact: true` — the maintainer's daily driver):** same workers, same pipeline. But observations go to per-session disk buffers and compaction only happens when you run `/blackhole`. Cleaner conversation, manual schedule.
|
|
133
216
|
|
|
134
217
|
The tradeoff is simplicity vs cleanliness:
|
|
135
218
|
|
|
136
219
|
| | Auto (default) | Manual (`noAutoCompact: true`) |
|
|
137
220
|
|---|---|---|
|
|
138
221
|
| Workers run? | Yes | Yes |
|
|
139
|
-
| Observations go to | Conversation markers (invisible in TUI) | Disk (
|
|
140
|
-
| Observations accumulate across runs | Branch markers (replaced each cycle) | Pending batches — `/memory` shows pending counts |
|
|
222
|
+
| Observations go to | Conversation markers (invisible in TUI) | Disk (`<sessionId>-pending.json`) |
|
|
223
|
+
| Observations accumulate across runs | Branch markers (replaced each cycle) | Pending batches accumulated — `/blackhole-memory` shows pending counts |
|
|
141
224
|
| Auto-compact on `agent_end` | Yes | No |
|
|
142
225
|
| `/blackhole` | Optional — use it whenever you want | Required to flush + compact |
|
|
143
|
-
| Conversation history | OM marker entries between turns (they exist but
|
|
226
|
+
| Conversation history | OM marker entries between turns (they exist but don't clutter the display) | Clean — nothing between turns |
|
|
144
227
|
| Use case | "I don't want to think about it" | "I want to control when context gets compressed" |
|
|
145
228
|
|
|
146
229
|
**Does `/blackhole` work like a single `/compact` that Just Works?**
|
|
147
230
|
|
|
148
|
-
Yes, that's exactly the idea, especially in manual mode. When you feel context is getting full or accuracy is slipping, type `/blackhole`. It flushes any accumulated observations from disk, runs algorithmic vcc compaction (zero LLM cost), and injects your durable reflections into the replacement block. One command, everything gets compressed while keeping your
|
|
231
|
+
Yes, that's exactly the idea, especially in manual mode. When you feel context is getting full or accuracy is slipping, type `/blackhole`. It flushes any accumulated observations from disk, runs algorithmic vcc compaction (zero LLM cost), and injects your durable reflections into the replacement block. One command, everything gets compressed while keeping your session memory alive.
|
|
149
232
|
|
|
150
233
|
The difference from Pi's built-in `/compact`:
|
|
151
234
|
- `/compact` calls an LLM to write a free-form summary — costly, lossy, no memory layer.
|
|
152
235
|
- `/blackhole` uses algorithmic section extraction (goals, files, commits, preferences...) plus injects observations/reflections from the session ledger. No LLM involved in the compaction itself. Fast, deterministic, memory-preserving.
|
|
153
236
|
|
|
154
|
-
###
|
|
237
|
+
### Passive mode
|
|
238
|
+
|
|
239
|
+
Set `passive: true` (or the environment variable `PI_BLACKHOLE_PASSIVE=true`) to completely disable all background workers AND auto-compaction. The extension becomes a no-op until you explicitly run `/blackhole`. This is useful for debugging or if you want zero background activity.
|
|
240
|
+
|
|
241
|
+
### Without observational memory (vcc-only)
|
|
155
242
|
|
|
156
|
-
Set `memory: false` or run `/blackhole om-off` for pure
|
|
243
|
+
Set `memory: false` or run `/blackhole om-off` for pure vcc compaction — no background workers, no memory injection. The compaction still uses the algorithmic vcc pipeline (not Pi's LLM-based compaction). Re-enable with `/blackhole om-on` or setting `memory: true`.
|
|
244
|
+
|
|
245
|
+
This is a lighter alternative to `passive`: workers are off but auto-compaction still runs.
|
|
157
246
|
|
|
158
247
|
---
|
|
159
248
|
|
|
@@ -161,25 +250,35 @@ Set `memory: false` or run `/blackhole om-off` for pure pi-vcc compaction — no
|
|
|
161
250
|
|
|
162
251
|
| Command | What it does |
|
|
163
252
|
|---|---|
|
|
164
|
-
| `/blackhole` | Compact. `om-off` / `om-on` toggle observational memory. |
|
|
165
|
-
| `/blackhole-memory` | Pipeline status: token progress,
|
|
166
|
-
| `/blackhole-memory view` |
|
|
167
|
-
| `/blackhole-memory full` |
|
|
253
|
+
| `/blackhole` | Compact the conversation. Subcommands: `om-off` / `om-on` toggle observational memory. |
|
|
254
|
+
| `/blackhole-memory` (or `status`) | Pipeline status: token progress, observation/reflection counts, pending data, last errors |
|
|
255
|
+
| `/blackhole-memory view` | Show visible observations and reflections (after compaction trimming), copied to clipboard |
|
|
256
|
+
| `/blackhole-memory full` | Show ALL recorded memory (including dropped observations), copied to clipboard |
|
|
168
257
|
| `/blackhole-recall <query>` | Search session history. Supports `page:N`, `scope:all`, `mode:file|transcript|touched` |
|
|
169
258
|
|
|
170
259
|
## Tools
|
|
171
260
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
|
261
|
+
The agent gets a unified `recall` tool that handles three types of input:
|
|
262
|
+
|
|
263
|
+
| Input | What it does |
|
|
264
|
+
|---|---|
|
|
265
|
+
| `[12-char hex]` | Recover source evidence for a specific observation or reflection ID from the session ledger |
|
|
266
|
+
| `#N` | Expand a session entry by index (show full content) |
|
|
267
|
+
| `#N:path` | Drill-down into file content from a tool call (e.g. `#42:auth.ts` shows first 30 lines; `#42:auth.ts:30` shows next 30; `#42:auth.ts:full` shows everything) |
|
|
268
|
+
| Free text | BM25-ranked OR search across transcript and/or file content. Rare terms weighted higher. |
|
|
269
|
+
| `mode:file` | Search only write/edit file content |
|
|
270
|
+
| `mode:transcript` | Search only conversation text |
|
|
271
|
+
| `mode:touched` | Aggregate all files written/edited, grouped by path with entry indices |
|
|
272
|
+
| Regex | Pattern search (e.g. `fork.*pi-vcc`, `hook|inject`) |
|
|
273
|
+
| `scope:all` | Search across all session lineages, not just the active one |
|
|
175
274
|
|
|
176
275
|
---
|
|
177
276
|
|
|
178
277
|
## Configuration
|
|
179
278
|
|
|
180
|
-
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
|
|
279
|
+
All settings in a single JSON file: **`~/.pi/agent/pi-blackhole/pi-blackhole-config.json`** — auto-created with defaults on first startup. See [`CONFIG.md`](CONFIG.md) for the full reference with detailed explanations for every knob. An annotated example config is at [`example-config.json`](example-config.json).
|
|
181
280
|
|
|
182
|
-
Quick start
|
|
281
|
+
Quick start — just set custom models (if you want):
|
|
183
282
|
|
|
184
283
|
```json
|
|
185
284
|
{
|
|
@@ -189,36 +288,41 @@ Quick start with custom models:
|
|
|
189
288
|
}
|
|
190
289
|
```
|
|
191
290
|
|
|
291
|
+
That's it. Everything else has sensible defaults.
|
|
292
|
+
|
|
192
293
|
### Settings at a glance
|
|
193
294
|
|
|
194
|
-
| Setting | Default
|
|
295
|
+
| Setting | Default | What it controls |
|
|
195
296
|
|---|---|---|
|
|
196
|
-
| `overrideDefaultCompaction` | `false` | Route
|
|
197
|
-
| `noAutoCompact` | `false` | Manual mode: save to disk, disable auto-compaction |
|
|
198
|
-
| `memory` | `true` | `false` =
|
|
199
|
-
| `passive` | `false` | Disable all workers
|
|
200
|
-
| `model` | — | Base fallback model for all workers |
|
|
201
|
-
| `observerModel` / `observerFallbackModels` | — / `[]` | Primary +
|
|
202
|
-
| `reflectorModel` / `reflectorFallbackModels` | — / `[]` | Primary +
|
|
203
|
-
| `dropperModel` / `dropperFallbackModels` | — / `[]` | Primary +
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
297
|
+
| `overrideDefaultCompaction` | `false` | Route ALL Pi compactions through blackhole, not just explicit `/blackhole` |
|
|
298
|
+
| `noAutoCompact` | `false` | Manual mode: save observations to disk, disable auto-compaction |
|
|
299
|
+
| `memory` | `true` | `false` = vcc only, no workers, no OM injection |
|
|
300
|
+
| `passive` | `false` | Disable all workers AND auto-compaction entirely |
|
|
301
|
+
| `model` | — | Base fallback model for all workers (last resort before session model) |
|
|
302
|
+
| `observerModel` / `observerFallbackModels` | — / `[]` | Primary + fallback models for observer (extracts facts) |
|
|
303
|
+
| `reflectorModel` / `reflectorFallbackModels` | — / `[]` | Primary + fallback models for reflector (synthesizes reflections) |
|
|
304
|
+
| `dropperModel` / `dropperFallbackModels` | — / `[]` | Primary + fallback models for dropper (prunes observations) |
|
|
305
|
+
| *(per model)* `thinking` | `"low"` | Thinking/reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
|
|
306
|
+
| *(per model)* `cooldownHours` | `1` | How long to skip this model after a retryable error |
|
|
307
|
+
| `observeAfterTokens` | `15000` | Min accumulated tokens before observer runs |
|
|
308
|
+
| `reflectAfterTokens` | `25000` | Min accumulated tokens before reflector + dropper run |
|
|
309
|
+
| `compactAfterTokens` | `81000` | Auto-compaction threshold (when `noAutoCompact` is `false`) |
|
|
207
310
|
| `observerChunkMaxTokens` | `40000` | Max observer input per run (newest-first) |
|
|
208
|
-
| `observerPreambleMaxTokens` | `0` (auto) |
|
|
311
|
+
| `observerPreambleMaxTokens` | `0` (auto) | Preamble cap for observer in `noAutoCompact` mode (auto = 30% of chunk) |
|
|
209
312
|
| `observationsPoolMaxTokens` | `20000` | Max active observation pool before dropper prunes |
|
|
313
|
+
| `observationsPoolTargetTokens` | `10000` | Target size dropper aims for after pruning (derived: half of pool max) |
|
|
210
314
|
| `reflectorInputMaxTokens` | `80000` | Max reflector input budget |
|
|
211
315
|
| `dropperInputMaxTokens` | `80000` | Max dropper input budget |
|
|
212
|
-
| `agentMaxTurns` | `16` | Max turns per worker
|
|
316
|
+
| `agentMaxTurns` | `16` | Max agent-loop turns per worker per run |
|
|
213
317
|
| `debug` | `false` | Pre-compaction snapshot to `/tmp/pi-blackhole-debug.json` |
|
|
214
|
-
| `debugLog` | `false` | Continuous JSONL log to `~/.pi/agent/pi-blackhole/debug.ndjson` |
|
|
318
|
+
| `debugLog` | `false` | Continuous JSONL debug log to `~/.pi/agent/pi-blackhole/debug.ndjson` |
|
|
215
319
|
|
|
216
|
-
|
|
320
|
+
**Environment override:** `PI_BLACKHOLE_PASSIVE=true` disables all workers without touching the config file.
|
|
217
321
|
|
|
218
322
|
### Configuration presets
|
|
219
323
|
|
|
220
324
|
The defaults above target a **medium-context** setup (~128k context window, e.g. GPT-4o, Claude Sonnet).
|
|
221
|
-
Paste the appropriate block into your config to match your model's context size.
|
|
325
|
+
Paste the appropriate block into your config to match your main session model's context size.
|
|
222
326
|
|
|
223
327
|
#### Low context (~32k-64k — older models, fast budget models)
|
|
224
328
|
|
|
@@ -237,12 +341,12 @@ Paste the appropriate block into your config to match your model's context size.
|
|
|
237
341
|
|
|
238
342
|
#### Medium context (~128k — GPT-4o, Claude Sonnet, Gemini Pro; this is the default)
|
|
239
343
|
|
|
240
|
-
|
|
344
|
+
These are the built-in defaults. If you reset your config, these are what you get:
|
|
241
345
|
|
|
242
346
|
```json
|
|
243
347
|
{
|
|
244
|
-
"observeAfterTokens":
|
|
245
|
-
"reflectAfterTokens":
|
|
348
|
+
"observeAfterTokens": 15000,
|
|
349
|
+
"reflectAfterTokens": 25000,
|
|
246
350
|
"compactAfterTokens": 81000,
|
|
247
351
|
"observerChunkMaxTokens": 40000,
|
|
248
352
|
"observerPreambleMaxTokens": 0,
|
|
@@ -267,19 +371,31 @@ Our built-in defaults already target this tier. If you reset your config, these
|
|
|
267
371
|
}
|
|
268
372
|
```
|
|
269
373
|
|
|
270
|
-
**What to tune first:** `compactAfterTokens` should be significantly below your model's total context window — aim for ~60-70%. If
|
|
374
|
+
**What to tune first:** `compactAfterTokens` should be significantly below your model's total context window — aim for ~60-70%. If the agent loses context before compaction fires, lower it. If compaction fires too often and breaks flow, raise it. The other thresholds scale proportionally.
|
|
375
|
+
|
|
376
|
+
### Tip: comments in config
|
|
377
|
+
|
|
378
|
+
The config preserves unknown keys, so you can add `_comment` or `_notes` fields to document your choices inline. They're ignored by the parser.
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"_comment": "Tuned for my Cerebras + OpenRouter free model setup",
|
|
383
|
+
"observerModel": { "provider": "openrouter", "id": "qwen/qwen3-next-80b-a3b-instruct:free", "thinking": "low" }
|
|
384
|
+
}
|
|
385
|
+
```
|
|
271
386
|
|
|
272
387
|
---
|
|
273
388
|
|
|
274
389
|
## Model fallback chains
|
|
275
390
|
|
|
276
|
-
Each worker has a primary model and an ordered fallback list. On any error — rate limit, timeout, API failure — the failed model is cooled down and the next candidate is tried. If all candidates are exhausted, the pipeline aborts and retries on the next trigger event. The session model is always the last resort and is never cooled down.
|
|
391
|
+
Each worker has a primary model and an ordered fallback list. On any error — rate limit, timeout, API failure, 5xx — the failed model is cooled down and the next candidate is tried. If all candidates are exhausted, the pipeline aborts and retries on the next trigger event. The session model is always the last resort and is never cooled down.
|
|
277
392
|
|
|
278
393
|
```
|
|
279
|
-
[Worker fails: 429 / timeout / 5xx]
|
|
394
|
+
[Worker fails: 429 / timeout / 5xx / connection error]
|
|
280
395
|
│
|
|
281
396
|
v
|
|
282
397
|
Add model to cooldown list
|
|
398
|
+
(persisted to pi-blackhole-cooldown.json)
|
|
283
399
|
│
|
|
284
400
|
v
|
|
285
401
|
Try next fallback candidate
|
|
@@ -293,16 +409,54 @@ Each worker has a primary model and an ordered fallback list. On any error — r
|
|
|
293
409
|
Fall back to session model (never cooled down)
|
|
294
410
|
```
|
|
295
411
|
|
|
296
|
-
Cooldowns
|
|
412
|
+
Cooldowns survive Pi restarts — they're persisted to `~/.pi/agent/pi-blackhole/pi-blackhole-cooldown.json`. Each entry records the model identifier, the triggering error, which stage failed, and the expiry timestamp.
|
|
413
|
+
|
|
414
|
+
### Resolution order
|
|
415
|
+
|
|
416
|
+
For each stage, the runtime builds a candidate list from:
|
|
417
|
+
|
|
418
|
+
1. **Primary stage model** (`observerModel`, `reflectorModel`, `dropperModel`)
|
|
419
|
+
2. **Stage fallback models** (`observerFallbackModels`, etc.) — tried in order
|
|
420
|
+
3. **Base model** (`model` — shared across all workers)
|
|
421
|
+
4. **Session model** (the model used for your main conversation — always the last resort)
|
|
422
|
+
|
|
423
|
+
Models with active cooldowns are transparently skipped. The runtime tries up to 10 model resolutions per stage before giving up entirely.
|
|
424
|
+
|
|
425
|
+
### Example: full resolution chain
|
|
297
426
|
|
|
298
|
-
With a
|
|
427
|
+
With a fully configured setup:
|
|
299
428
|
|
|
300
429
|
```
|
|
301
|
-
Observer: qwen3-next-80b → gemma4:31b-cloud → gemma-4-31b-it:free → base model → session model
|
|
302
|
-
Reflector: gpt-oss-120b (cerebras) → glm-4.7 (z.ai) → gpt-oss-120b:free → base model → session model
|
|
303
|
-
Dropper: gpt-oss-120b (cerebras) → glm-4.7 (z.ai) → gpt-oss-120b:free → base model → session model
|
|
430
|
+
Observer: qwen3-next-80b (openrouter) → gemma4:31b-cloud (ollama) → gemma-4-31b-it:free (openrouter) → base model → session model
|
|
431
|
+
Reflector: gpt-oss-120b (cerebras) → glm-4.7 (z.ai) → gpt-oss-120b:free (openrouter) → base model → session model
|
|
432
|
+
Dropper: gpt-oss-120b (cerebras) → glm-4.7 (z.ai) → gpt-oss-120b:free (openrouter) → base model → session model
|
|
304
433
|
```
|
|
305
434
|
|
|
435
|
+
### Per-model thinking levels
|
|
436
|
+
|
|
437
|
+
Each model config supports a `thinking` field that controls reasoning effort:
|
|
438
|
+
|
|
439
|
+
```json
|
|
440
|
+
{
|
|
441
|
+
"observerModel": {
|
|
442
|
+
"provider": "openrouter",
|
|
443
|
+
"id": "qwen/qwen3-next-80b-a3b-instruct:free",
|
|
444
|
+
"thinking": "low", ← reasoning effort for this specific model
|
|
445
|
+
"cooldownHours": 12 ← custom cooldown duration
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Valid values: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. Not all models support every level.
|
|
451
|
+
|
|
452
|
+
### Retryable error detection
|
|
453
|
+
|
|
454
|
+
The runtime uses a regex to detect retryable errors — it looks for patterns like `rate limit`, `429`, `5xx`, `timeout`, `service unavailable`, `connection error`, `websocket closed`, etc. Non-retryable errors (auth failures, invalid model IDs) immediately skip that candidate and move to the next.
|
|
455
|
+
|
|
456
|
+
### 30-second retry gate
|
|
457
|
+
|
|
458
|
+
After any stage fails completely, the pipeline waits 30 seconds before attempting another consolidation run. This prevents rapid retry loops that would waste API calls on the same failing models.
|
|
459
|
+
|
|
306
460
|
---
|
|
307
461
|
|
|
308
462
|
## Recall
|
|
@@ -311,64 +465,42 @@ Pi's default compaction discards old messages permanently — after compaction,
|
|
|
311
465
|
|
|
312
466
|
### `recall` tool (agent-facing)
|
|
313
467
|
|
|
314
|
-
|
|
468
|
+
The agent gets one unified tool that searches session history, expands entries, drills into file content, and looks up observational memory. Searches read the raw session file directly, bypassing compaction.
|
|
315
469
|
|
|
316
470
|
| Input | What it does |
|
|
317
471
|
|---|---|
|
|
318
|
-
| `[
|
|
319
|
-
| `#N` | Expand a
|
|
320
|
-
| `#N:path` | Drill-down into file content from a tool call (e.g. `#42:auth.ts`) |
|
|
321
|
-
|
|
|
322
|
-
|
|
|
323
|
-
|
|
|
324
|
-
| `mode:
|
|
325
|
-
| `mode:transcript` | Search only conversation text (not file content) |
|
|
326
|
-
| `mode:touched` | Aggregate view of all files written/edited, grouped by path with entry indices |
|
|
472
|
+
| `[12-char hex]` | Recover source evidence for an observation or reflection ID from the session ledger |
|
|
473
|
+
| `#N` | Expand a session entry by index (show full content, not truncated) |
|
|
474
|
+
| `#N:path` | Drill-down into file content from a tool call (e.g. `#42:auth.ts` shows first 30 lines; `#42:auth.ts:30` shows next 30; `#42:auth.ts:full` shows everything) |
|
|
475
|
+
| Free text | BM25-ranked OR search across transcript + file indicators. Rare terms weighted higher. |
|
|
476
|
+
| `mode:file` | Search only write/edit file content |
|
|
477
|
+
| `mode:transcript` | Search only conversation text |
|
|
478
|
+
| `mode:touched` | Aggregate all files written/edited across the session, grouped by path with entry indices |
|
|
327
479
|
| Regex | Pattern search (e.g. `fork.*pi-vcc`, `hook\|inject`) |
|
|
480
|
+
| `scope:all` | Search across all session lineages (default: active lineage only) |
|
|
481
|
+
|
|
482
|
+
**OM coupling:** When expanding session entries (`#N`), the tool automatically looks up related observations and reflections from the session ledger. If any of your expanded entries are referenced as source evidence by an observation, those observations are shown alongside the expanded content.
|
|
328
483
|
|
|
329
484
|
### `/blackhole-recall` command (user-facing)
|
|
330
485
|
|
|
331
|
-
Results are shown as a collapsible message and auto-fed to the agent as context.
|
|
486
|
+
Results are shown as a collapsible message and auto-fed to the agent as context. Same engine as the `recall` tool.
|
|
332
487
|
|
|
333
488
|
```
|
|
334
|
-
/blackhole-recall auth token
|
|
335
|
-
/blackhole-recall auth token page:2
|
|
336
|
-
/blackhole-recall hook|inject
|
|
337
|
-
/blackhole-recall fail.*build scope:all
|
|
338
|
-
/blackhole-recall mode:file
|
|
339
|
-
/blackhole-recall mode:transcript
|
|
340
|
-
/blackhole-recall mode:touched
|
|
341
|
-
/blackhole-recall
|
|
489
|
+
/blackhole-recall auth token # active-lineage search, ranked
|
|
490
|
+
/blackhole-recall auth token page:2 # paginated (5 results/page)
|
|
491
|
+
/blackhole-recall hook|inject # regex
|
|
492
|
+
/blackhole-recall fail.*build scope:all # regex across all lineages
|
|
493
|
+
/blackhole-recall mode:file # search only write/edit file content
|
|
494
|
+
/blackhole-recall mode:transcript # search only conversation
|
|
495
|
+
/blackhole-recall mode:touched # aggregate view of all files touched
|
|
496
|
+
/blackhole-recall # recent 25 entries
|
|
342
497
|
```
|
|
343
498
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
## Install
|
|
499
|
+
### Details
|
|
347
500
|
|
|
348
|
-
|
|
349
|
-
# From npm (recommended)
|
|
350
|
-
pi install npm:pi-blackhole
|
|
351
|
-
|
|
352
|
-
# Or directly from GitHub
|
|
353
|
-
pi install git:github.com/k0valik/pi-blackhole
|
|
354
|
-
```
|
|
501
|
+
**File drill-down** reads the raw session JSONL to extract file content from tool call operations. Supports offset/limit paging so you can browse long files. Note: edit diffs are not indexed for text search — drill-down reads them from the raw session as original full-file writes.
|
|
355
502
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
```bash
|
|
359
|
-
pi uninstall https://github.com/sting8k/pi-vcc
|
|
360
|
-
pi uninstall https://github.com/elpapi42/pi-observational-memory
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
Then `/reload` or restart Pi.
|
|
364
|
-
|
|
365
|
-
### Standalone install (without Pi)
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
npm install pi-blackhole
|
|
369
|
-
# or
|
|
370
|
-
pnpm add pi-blackhole
|
|
371
|
-
```
|
|
503
|
+
**Touched mode** (`mode:touched`) aggregates all files written, edited, or read across the session, grouped by path. Each entry shows which tool operation touched the file and the line count. Useful for getting a lay of the land after a long session.
|
|
372
504
|
|
|
373
505
|
## Uninstall
|
|
374
506
|
|
|
@@ -381,12 +513,24 @@ rm -rf ~/.pi/agent/pi-blackhole
|
|
|
381
513
|
|
|
382
514
|
## Credits
|
|
383
515
|
|
|
384
|
-
|
|
516
|
+
pi-blackhole started as a merge of two upstream projects, but has since diverged significantly. The codebase still carries DNA from both:
|
|
517
|
+
|
|
518
|
+
- **[pi-vcc](https://github.com/sting8k/pi-vcc)** by @sting8k — algorithmic conversation compaction (the `compile()` pipeline, section extraction, recall core)
|
|
519
|
+
- **[pi-observational-memory](https://github.com/elpapi42/pi-observational-memory)** by @elpapi42 — session-ledger-based observation/reflection capture, memory agents, ledger folding
|
|
385
520
|
|
|
386
|
-
|
|
387
|
-
- **[pi-observational-memory](https://github.com/elpapi42/pi-observational-memory)** by @elpapi42 — session-ledger-based observation and reflection capture
|
|
521
|
+
What blackhole adds and reworks on top:
|
|
388
522
|
|
|
389
|
-
|
|
523
|
+
- **Unified configuration** — one JSON file, not two
|
|
524
|
+
- **Per-worker model fallback chains** with persisted cooldowns that survive Pi restarts
|
|
525
|
+
- **Manual flush mode** — `noAutoCompact` saves observations to per-session disk buffers
|
|
526
|
+
- **Conflict resolution** — OM hooks into vcc's compaction, not Pi's default
|
|
527
|
+
- **Memory toggle** (`/blackhole om-off` / `/blackhole om-on`) — disable the memory layer without uninstalling
|
|
528
|
+
- **Per-session pending state** — isolated per-session JSON files, no cross-session contamination
|
|
529
|
+
- **Custom provider bridge** — consolidation agents loaded via jiti can still use provider stream functions registered by other extensions
|
|
530
|
+
- **Retryable error detection with per-model cooldowns** — models that fail get cooled down, fallbacks tried automatically, 30-second retry gate prevents spam
|
|
531
|
+
- **Improved observer/reflector/dropper prompts** — each heavily customized with detailed extraction rules, relevance guidance, and error handling
|
|
532
|
+
- **OM-recall coupling** — when expanding session entries via `recall`, related observations and reflections are automatically shown
|
|
533
|
+
- **Thinking level support** — per-model `thinking` field for reasoning effort control
|
|
390
534
|
|
|
391
535
|
## License
|
|
392
536
|
|
package/example-config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "blackhole unified configuration — ~/.pi/agent/pi-blackhole-config.json",
|
|
2
|
+
"_comment": "blackhole unified configuration — ~/.pi/agent/pi-blackhole/pi-blackhole-config.json",
|
|
3
3
|
"_all_settings_in_one_file": true,
|
|
4
4
|
|
|
5
5
|
"overrideDefaultCompaction": false,
|
|
@@ -90,12 +90,14 @@
|
|
|
90
90
|
"reflectAfterTokens": 25000,
|
|
91
91
|
"compactAfterTokens": 81000,
|
|
92
92
|
"observationsPoolMaxTokens": 20000,
|
|
93
|
+
"observationsPoolTargetTokens": 10000,
|
|
93
94
|
"reflectorInputMaxTokens": 80000,
|
|
94
95
|
"dropperInputMaxTokens": 80000,
|
|
95
96
|
"observerChunkMaxTokens": 40000,
|
|
96
97
|
"observerPreambleMaxTokens": 0,
|
|
97
98
|
"agentMaxTurns": 16,
|
|
98
99
|
|
|
100
|
+
"memory": true,
|
|
99
101
|
"passive": false,
|
|
100
102
|
"noAutoCompact": false,
|
|
101
103
|
"debugLog": false,
|
|
@@ -114,7 +116,9 @@
|
|
|
114
116
|
"Env override for passive mode: PI_BLACKHOLE_PASSIVE=true.",
|
|
115
117
|
"",
|
|
116
118
|
"noAutoCompact mode accumulates observationBatches/reflectionBatches in",
|
|
117
|
-
"pending.json across pipeline runs.
|
|
119
|
+
"<sessionId>-pending.json (per-session) across pipeline runs.",
|
|
120
|
+
"On /blackhole flush, all accumulated batches are written to the branch and the file is cleared.",
|
|
121
|
+
"The observer preamble (CURRENT OBSERVATIONS)",
|
|
118
122
|
"is capped to observerPreambleMaxTokens (default 0 = auto 30% of observerChunkMaxTokens).",
|
|
119
123
|
"This is a soft safety valve — it only trims when accumulated observations exceed",
|
|
120
124
|
"the budget. High-relevance observations are always kept. Reflections are never trimmed.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-blackhole",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"packageManager": "pnpm@11.2.2",
|
|
5
5
|
"description": "Unified compaction + observational memory extension for Pi — compresses conversation context while preserving durable observations and reflections",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,6 +14,7 @@ import { compile } from "../core/summarize";
|
|
|
14
14
|
import type { PiVccCompactionDetails } from "../details";
|
|
15
15
|
import { buildCompactionProjection, renderSummary } from "../om/ledger/index.js";
|
|
16
16
|
import type { Runtime } from "../om/runtime.js";
|
|
17
|
+
import { debugLog } from "../om/debug-log.js";
|
|
17
18
|
|
|
18
19
|
export const PI_VCC_COMPACT_INSTRUCTION = "__pi_vcc__";
|
|
19
20
|
|
|
@@ -141,14 +142,28 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
141
142
|
pi.on("session_before_compact", (event, ctx) => {
|
|
142
143
|
const { preparation, branchEntries, customInstructions } = event;
|
|
143
144
|
omRuntime.ensureConfig(ctx.cwd ?? process.cwd());
|
|
145
|
+
const trace = (ev: string, d?: Record<string, unknown>) => debugLog(ev, d, omRuntime.config.debugLog === true);
|
|
146
|
+
|
|
147
|
+
trace("before_compact.enter", {
|
|
148
|
+
customInstructions,
|
|
149
|
+
isPiVcc: customInstructions === PI_VCC_COMPACT_INSTRUCTION,
|
|
150
|
+
overrideDefaultCompaction: omRuntime.config.overrideDefaultCompaction,
|
|
151
|
+
noAutoCompact: omRuntime.config.noAutoCompact,
|
|
152
|
+
branchLength: branchEntries.length,
|
|
153
|
+
hasPreviousSummary: !!preparation.previousSummary,
|
|
154
|
+
});
|
|
144
155
|
|
|
145
156
|
// Always handle explicit /blackhole marker.
|
|
146
157
|
// Otherwise, only handle when user opted in via settings.
|
|
147
158
|
const isPiVcc = customInstructions === PI_VCC_COMPACT_INSTRUCTION;
|
|
148
|
-
if (!isPiVcc && !omRuntime.config.overrideDefaultCompaction)
|
|
159
|
+
if (!isPiVcc && !omRuntime.config.overrideDefaultCompaction) {
|
|
160
|
+
trace("before_compact.return_early", { reason: "overrideDefaultCompaction=false and not /blackhole" });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
149
163
|
|
|
150
164
|
// When noAutoCompact is active, only /blackhole can trigger compaction
|
|
151
165
|
if (omRuntime.config.noAutoCompact && !isPiVcc) {
|
|
166
|
+
trace("before_compact.cancel", { reason: "noAutoCompact and not /blackhole" });
|
|
152
167
|
return { cancel: true };
|
|
153
168
|
}
|
|
154
169
|
|
|
@@ -212,12 +227,20 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
212
227
|
})),
|
|
213
228
|
});
|
|
214
229
|
|
|
230
|
+
trace("before_compact.cancel", { reason: ownCut.reason, isPiVcc });
|
|
215
231
|
try {
|
|
216
232
|
ctx?.ui?.notify?.(REASON_MESSAGES[ownCut.reason], "warning");
|
|
217
233
|
} catch {}
|
|
218
234
|
return { cancel: true };
|
|
219
235
|
}
|
|
220
236
|
|
|
237
|
+
trace("before_compact.proceeding", {
|
|
238
|
+
messageCount: ownCut.messages.length,
|
|
239
|
+
firstKeptEntryId: ownCut.firstKeptEntryId,
|
|
240
|
+
compactAll: ownCut.compactAll,
|
|
241
|
+
isPiVcc,
|
|
242
|
+
});
|
|
243
|
+
|
|
221
244
|
const agentMessages = ownCut.messages;
|
|
222
245
|
const firstKeptEntryId = ownCut.firstKeptEntryId;
|
|
223
246
|
const messages = convertToLlm(agentMessages);
|
|
@@ -278,6 +301,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
278
301
|
sections: [...summary.matchAll(/^\[(.+?)\]/gm)].map((m) => m[1]),
|
|
279
302
|
});
|
|
280
303
|
|
|
304
|
+
trace("before_compact.summary_generated", { summaryLength: summary.length, messageCount: agentMessages.length });
|
|
305
|
+
|
|
281
306
|
const details: PiVccCompactionDetails = {
|
|
282
307
|
compactor: "blackhole",
|
|
283
308
|
version: 1,
|
|
@@ -291,6 +316,7 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI, omRuntime: Runtime)
|
|
|
291
316
|
// ── Inject observational-memory content ───────────────────────────
|
|
292
317
|
let omContent = "";
|
|
293
318
|
let omDetails: Record<string, unknown> | undefined;
|
|
319
|
+
trace("before_compact.om_injection", { memoryEnabled: omRuntime.config.memory !== false });
|
|
294
320
|
if (omRuntime.config.memory !== false) {
|
|
295
321
|
const projection = buildCompactionProjection(
|
|
296
322
|
branchEntries as any[],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { rawTokensSinceLastCompaction, type Entry } from "./ledger/index.js";
|
|
3
3
|
import type { Runtime } from "./runtime.js";
|
|
4
|
+
import { debugLog } from "./debug-log.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Regex matching Pi's internal retryable error detection.
|
|
@@ -13,10 +14,44 @@ const RETRYABLE_ERROR_RE =
|
|
|
13
14
|
export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
|
|
14
15
|
pi.on("agent_end", (event: any, ctx: any) => {
|
|
15
16
|
runtime.ensureConfig(ctx.cwd);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
|
|
18
|
+
// Pass the config flag explicitly — this handler runs outside ALS context
|
|
19
|
+
// (agent_end events don't flow through consolidation's withDebugLogContext),
|
|
20
|
+
// and the setTimeout callback would lose ALS context anyway.
|
|
21
|
+
const dbg = (ev: string, d?: Record<string, unknown>) => debugLog(ev, d, runtime.config.debugLog === true);
|
|
22
|
+
|
|
23
|
+
dbg("compaction_trigger.agent_end", {
|
|
24
|
+
passive: runtime.config.passive,
|
|
25
|
+
memory: runtime.config.memory,
|
|
26
|
+
noAutoCompact: runtime.config.noAutoCompact,
|
|
27
|
+
overrideDefaultCompaction: runtime.config.overrideDefaultCompaction,
|
|
28
|
+
compactInFlight: runtime.compactInFlight,
|
|
29
|
+
compactAfterTokens: runtime.config.compactAfterTokens,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (runtime.config.passive === true) {
|
|
33
|
+
dbg("compaction_trigger.skip", { reason: "passive" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (runtime.config.memory === false) {
|
|
37
|
+
dbg("compaction_trigger.skip", { reason: "memory" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (runtime.config.noAutoCompact === true) {
|
|
41
|
+
dbg("compaction_trigger.skip", { reason: "noAutoCompact" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Don't force Pi to compact unless the user explicitly opted into blackhole's pipeline.
|
|
45
|
+
// When overrideDefaultCompaction is false (default), blackhole stays out of the way
|
|
46
|
+
// and lets Pi handle its own compaction naturally.
|
|
47
|
+
if (runtime.config.overrideDefaultCompaction === false) {
|
|
48
|
+
dbg("compaction_trigger.skip", { reason: "overrideDefaultCompaction_false" });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (runtime.compactInFlight) {
|
|
52
|
+
dbg("compaction_trigger.skip", { reason: "compactInFlight" });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
20
55
|
|
|
21
56
|
// Don't trigger compaction if Pi will auto-retry — the agent hasn't truly finished.
|
|
22
57
|
// Pi emits agent_end before its own retry check, so we must detect this ourselves.
|
|
@@ -34,8 +69,14 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
72
|
+
dbg("compaction_trigger.branch_check", { branchLength: entries.length, hasLastEntry: entries.length > 0, lastEntryType: entries.length > 0 ? entries[entries.length - 1].type : "none" });
|
|
73
|
+
|
|
37
74
|
const tokens = rawTokensSinceLastCompaction(entries);
|
|
38
|
-
|
|
75
|
+
dbg("compaction_trigger.tokens", { tokens, compactAfterTokens: runtime.config.compactAfterTokens, branchLength: entries.length });
|
|
76
|
+
if (tokens < runtime.config.compactAfterTokens) {
|
|
77
|
+
dbg("compaction_trigger.skip", { reason: "below_threshold", tokens, threshold: runtime.config.compactAfterTokens });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
39
80
|
|
|
40
81
|
// Capture ctx properties synchronously — the deferred callback below
|
|
41
82
|
// may outlive the extension ctx (stale after session replacement/reload).
|
|
@@ -43,18 +84,25 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
43
84
|
const ui = ctx.ui;
|
|
44
85
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
45
86
|
|
|
87
|
+
dbg("compaction_trigger.threshold_reached", { tokens, sessionId, hasUI });
|
|
88
|
+
|
|
46
89
|
if (hasUI) ui?.notify(
|
|
47
90
|
`Observational memory: compaction threshold reached (~${tokens.toLocaleString()} tokens); triggering compaction`,
|
|
48
91
|
"info",
|
|
49
92
|
);
|
|
50
93
|
|
|
51
94
|
runtime.compactInFlight = true;
|
|
52
|
-
|
|
95
|
+
dbg("compaction_trigger.scheduled", { compactInFlight: runtime.compactInFlight });
|
|
96
|
+
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
dbg("compaction_trigger.microtask.enter", {});
|
|
53
99
|
try {
|
|
54
100
|
// Validate session identity — bail if the session was replaced/reloaded.
|
|
55
101
|
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
102
|
+
dbg("compaction_trigger.microtask.session_check", { currentSessionId, expectedSessionId: sessionId, match: currentSessionId === sessionId });
|
|
56
103
|
if (currentSessionId !== sessionId) {
|
|
57
104
|
runtime.compactInFlight = false;
|
|
105
|
+
dbg("compaction_trigger.microtask.bail", { reason: "session_changed" });
|
|
58
106
|
if (hasUI) ui?.notify(
|
|
59
107
|
"Observational memory: compaction cancelled — session changed before compaction",
|
|
60
108
|
"info",
|
|
@@ -62,8 +110,11 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
62
110
|
return;
|
|
63
111
|
}
|
|
64
112
|
|
|
65
|
-
|
|
113
|
+
const isIdle = ctx.isIdle();
|
|
114
|
+
dbg("compaction_trigger.microtask.idle_check", { isIdle });
|
|
115
|
+
if (!isIdle) {
|
|
66
116
|
runtime.compactInFlight = false;
|
|
117
|
+
dbg("compaction_trigger.microtask.bail", { reason: "not_idle" });
|
|
67
118
|
if (hasUI) ui?.notify(
|
|
68
119
|
"Observational memory: compaction deferred — agent became busy before compaction",
|
|
69
120
|
"info",
|
|
@@ -72,21 +123,27 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
72
123
|
}
|
|
73
124
|
const currentEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
74
125
|
const currentTokens = rawTokensSinceLastCompaction(currentEntries);
|
|
126
|
+
dbg("compaction_trigger.microtask.recheck_tokens", { currentTokens, threshold: runtime.config.compactAfterTokens, ok: currentTokens >= runtime.config.compactAfterTokens });
|
|
75
127
|
if (currentTokens < runtime.config.compactAfterTokens) {
|
|
76
128
|
runtime.compactInFlight = false;
|
|
129
|
+
dbg("compaction_trigger.microtask.bail", { reason: "pressure_relieved", currentTokens, threshold: runtime.config.compactAfterTokens });
|
|
77
130
|
if (hasUI) ui?.notify(
|
|
78
131
|
"Observational memory: compaction skipped — another compaction already ran before deferred compaction",
|
|
79
132
|
"info",
|
|
80
133
|
);
|
|
81
134
|
return;
|
|
82
135
|
}
|
|
136
|
+
|
|
137
|
+
dbg("compaction_trigger.microtask.calling_compact", {});
|
|
83
138
|
ctx.compact({
|
|
84
|
-
onComplete: () => {
|
|
139
|
+
onComplete: (result: any) => {
|
|
85
140
|
runtime.compactInFlight = false;
|
|
141
|
+
dbg("compaction_trigger.onComplete", { result: !!result });
|
|
86
142
|
if (hasUI) ui?.notify("Observational memory: compaction complete", "info");
|
|
87
143
|
},
|
|
88
144
|
onError: (error: { message: string }) => {
|
|
89
145
|
runtime.compactInFlight = false;
|
|
146
|
+
dbg("compaction_trigger.onError", { message: error?.message ?? String(error) });
|
|
90
147
|
if (error.message === "Compaction cancelled") {
|
|
91
148
|
// We already notified the user with the real reason before returning { cancel: true }.
|
|
92
149
|
return;
|
|
@@ -97,8 +154,9 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
97
154
|
} catch (error) {
|
|
98
155
|
runtime.compactInFlight = false;
|
|
99
156
|
const msg = error instanceof Error ? error.message : String(error);
|
|
157
|
+
dbg("compaction_trigger.microtask.error", { message: msg });
|
|
100
158
|
if (hasUI) ui?.notify(`Observational memory: compact threw: ${msg}`, "error");
|
|
101
159
|
}
|
|
102
|
-
});
|
|
160
|
+
}, 0);
|
|
103
161
|
});
|
|
104
162
|
}
|
package/src/om/debug-log.ts
CHANGED
|
@@ -25,9 +25,10 @@ export function withDebugLogContext<T>(context: DebugLogContext, fn: () => T): T
|
|
|
25
25
|
return storage.run({ ...parent, ...context }, fn);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function debugLog(event: string, data: Record<string, unknown> = {}): void {
|
|
28
|
+
export function debugLog(event: string, data: Record<string, unknown> = {}, forceEnabled?: boolean): void {
|
|
29
29
|
const context = storage.getStore();
|
|
30
|
-
|
|
30
|
+
const enabled = forceEnabled ?? context?.enabled ?? false;
|
|
31
|
+
if (enabled !== true) return;
|
|
31
32
|
|
|
32
33
|
try {
|
|
33
34
|
const path = join(getAgentDir(), DEBUG_LOG_RELATIVE_PATH);
|
|
@@ -36,8 +37,8 @@ export function debugLog(event: string, data: Record<string, unknown> = {}): voi
|
|
|
36
37
|
const payload = {
|
|
37
38
|
ts: new Date().toISOString(),
|
|
38
39
|
event,
|
|
39
|
-
cwd: context
|
|
40
|
-
runId: context
|
|
40
|
+
cwd: context?.cwd,
|
|
41
|
+
runId: context?.runId,
|
|
41
42
|
data,
|
|
42
43
|
};
|
|
43
44
|
appendFileSync(path, `${JSON.stringify(payload)}\n`, "utf-8");
|