opencode-blackhole 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/LICENSE.vendor +21 -0
- package/README.md +377 -2
- package/dist/server.js +5025 -0
- package/index.ts +1 -0
- package/package.json +57 -4
- package/src/adapter/opencode-messages.ts +270 -0
- package/src/compaction/build.ts +264 -0
- package/src/compaction/handoff.ts +421 -0
- package/src/config.ts +316 -0
- package/src/index.ts +714 -0
- package/src/memory/ledger.ts +254 -0
- package/src/memory/manager.ts +659 -0
- package/src/memory/models.ts +172 -0
- package/src/memory/prompts.ts +145 -0
- package/src/recall/recall.ts +327 -0
- package/src/state/paths.ts +42 -0
- package/src/store/capture.ts +84 -0
- package/src/store/transcript.ts +279 -0
- package/src/vcc/README.md +51 -0
- package/src/vcc/core/brief.ts +407 -0
- package/src/vcc/core/build-sections.ts +81 -0
- package/src/vcc/core/compact-args.ts +45 -0
- package/src/vcc/core/content.ts +156 -0
- package/src/vcc/core/drill-down.ts +302 -0
- package/src/vcc/core/filter-noise.ts +41 -0
- package/src/vcc/core/format-recall.ts +100 -0
- package/src/vcc/core/format.ts +104 -0
- package/src/vcc/core/lineage.ts +26 -0
- package/src/vcc/core/normalize.ts +76 -0
- package/src/vcc/core/rank.ts +283 -0
- package/src/vcc/core/recall-request.ts +160 -0
- package/src/vcc/core/recall-scope.ts +30 -0
- package/src/vcc/core/render-entries.ts +55 -0
- package/src/vcc/core/sanitize.ts +5 -0
- package/src/vcc/core/search-entries.ts +594 -0
- package/src/vcc/core/skill-collapse.ts +35 -0
- package/src/vcc/core/summarize.ts +289 -0
- package/src/vcc/core/token-estimate.ts +100 -0
- package/src/vcc/core/tool-args.ts +16 -0
- package/src/vcc/details.ts +11 -0
- package/src/vcc/extract/commits.ts +96 -0
- package/src/vcc/extract/files.ts +87 -0
- package/src/vcc/extract/goals.ts +79 -0
- package/src/vcc/extract/preferences.ts +55 -0
- package/src/vcc/extract/tracked-commands.ts +232 -0
- package/src/vcc/load-messages.ts +42 -0
- package/src/vcc/pi-types.ts +53 -0
- package/src/vcc/sections.ts +12 -0
- package/src/vcc/types.ts +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 opencode-blackhole contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This package contains code ported from:
|
|
26
|
+
|
|
27
|
+
pi-vcc - https://github.com/sting8k/pi-vcc (MIT) - Copyright (c) 2026 sting8k
|
|
28
|
+
pi-blackhole - https://github.com/k0valik/pi-blackhole (MIT) - Copyright (c) 2026 k0valik
|
|
29
|
+
|
|
30
|
+
The ported files live in `src/vcc/` - see `src/vcc/README.md` for exactly what
|
|
31
|
+
changed. Their licence text is in `LICENSE.vendor`.
|
|
32
|
+
|
|
33
|
+
The memory workers (observer, reflector, dropper) are an independent
|
|
34
|
+
implementation of pi-blackhole's design, written for this package; no code was
|
|
35
|
+
copied from it.
|
package/LICENSE.vendor
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sting8k
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,378 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-blackhole
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deterministic compaction + observational memory for **OpenCode**.
|
|
4
|
+
|
|
5
|
+
A port of [pi-blackhole](https://github.com/k0valik/pi-blackhole) (for the Pi agent)
|
|
6
|
+
to OpenCode V2, built on the [pi-vcc](https://github.com/sting8k/pi-vcc) compaction
|
|
7
|
+
engine (both MIT).
|
|
8
|
+
|
|
9
|
+
**What it does**
|
|
10
|
+
|
|
11
|
+
1. **Replaces OpenCode's LLM summarizer with a structured checkpoint.**
|
|
12
|
+
OpenCode's compaction calls the model to write a prose summary, then compacts
|
|
13
|
+
that summary, and the next one — details erode over a long session. This plugin
|
|
14
|
+
answers OpenCode's `compaction` hook itself. The inventory is extracted
|
|
15
|
+
deterministically — goal, files, commits, preferences, outstanding context,
|
|
16
|
+
with exact paths, commands and identifiers — and one small model call writes
|
|
17
|
+
the part no extractor can: `[Decisions]` (and why), `[Work State]`,
|
|
18
|
+
`[Next Move]` and `[Important Context]`. If that call fails, the deterministic
|
|
19
|
+
summary is used unchanged.
|
|
20
|
+
|
|
21
|
+
2. **Keeps durable memory across compactions.** A background observer records the
|
|
22
|
+
facts worth keeping (decisions, preferences, constraints, completed work); a
|
|
23
|
+
reflector distils them into durable insights. The memory is injected into the
|
|
24
|
+
system prompt on every request, and both the reflections and the observations
|
|
25
|
+
they rest on ride along with every compaction summary.
|
|
26
|
+
|
|
27
|
+
3. **Cuts token cost.** One bounded call replaces the summarizer's, the summary
|
|
28
|
+
has a hard token cap, and the workers run on cheap or free models.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
### One command
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
opencode plugin add opencode-blackhole
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That is the whole install. OpenCode fetches the package from the npm registry,
|
|
41
|
+
caches it in its own store (`~/.cache/opencode/npm/`) and adds it to your
|
|
42
|
+
global configuration — no `pnpm add`, no manual config editing.
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
opencode plugin list # see it
|
|
46
|
+
opencode plugin check # check for updates
|
|
47
|
+
opencode plugin update # update it
|
|
48
|
+
opencode plugin remove opencode-blackhole
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Restart OpenCode (or `/reload`) afterwards.
|
|
52
|
+
|
|
53
|
+
### The same thing by hand
|
|
54
|
+
|
|
55
|
+
If you prefer to edit the config yourself, add it to your **global** config
|
|
56
|
+
(`~/.config/opencode/opencode.json`) to use it everywhere, or to a project's
|
|
57
|
+
`opencode.json` for just that project:
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"$schema": "https://opencode.ai/config.json",
|
|
62
|
+
"plugins": ["opencode-blackhole"]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To remove it, prefix the name with `-` (it is applied last, so it also
|
|
67
|
+
cancels an auto-discovered copy):
|
|
68
|
+
|
|
69
|
+
```jsonc
|
|
70
|
+
{ "plugins": ["-opencode-blackhole"] }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### From a local checkout
|
|
74
|
+
|
|
75
|
+
```jsonc
|
|
76
|
+
{ "plugins": ["/absolute/path/to/opencode-blackhole"] }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
or, as a plugin file anywhere OpenCode looks for one:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// ~/.config/opencode/plugins/blackhole.ts
|
|
83
|
+
export { default } from "/absolute/path/to/opencode-blackhole/index.ts"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A checkout must be built first — the plugin loads a bundled `dist/server.js`:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
bun install && bun run build
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> Requires OpenCode V2 (`@opencode/plugin` ≥ 2.0.28) and Node ≥ 22.13.
|
|
93
|
+
|
|
94
|
+
### What works without any setup
|
|
95
|
+
|
|
96
|
+
The compaction half works immediately with whatever model you already use: it
|
|
97
|
+
replaces the summarizer call with a deterministic extraction, so it needs no
|
|
98
|
+
extra provider, no key and no cost.
|
|
99
|
+
|
|
100
|
+
The **memory** workers default to `openrouter/openrouter/free`, which needs an
|
|
101
|
+
OpenRouter account and key. Without one they fall back to **your session's
|
|
102
|
+
model** — so memory still works out of the box, at the cost of a background
|
|
103
|
+
call on the model you are already paying for. To keep the workers on a free
|
|
104
|
+
tier, give OpenRouter a key; to keep them off your main model entirely, set
|
|
105
|
+
`"sessionFallback": false` and point them at a model you choose:
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
{
|
|
109
|
+
"plugins": [
|
|
110
|
+
{
|
|
111
|
+
"package": "opencode-blackhole",
|
|
112
|
+
"options": {
|
|
113
|
+
"memory": {
|
|
114
|
+
"observerModel": { "providerID": "openai", "id": "gpt-4.1-mini" },
|
|
115
|
+
"reflectorModel": { "providerID": "openai", "id": "gpt-4.1-mini" }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The workers run through OpenCode's own generation API, so they use the
|
|
124
|
+
credentials you have connected in OpenCode — there is no second key to
|
|
125
|
+
configure. Two caveats:
|
|
126
|
+
|
|
127
|
+
- OpenCode's **free-tier models cannot be called from a plugin** ("OpenCode's
|
|
128
|
+
free tier can only be used from within OpenCode"), so if your session model is
|
|
129
|
+
one of those, it is not usable as the fallback. Use a provider model.
|
|
130
|
+
- A worker model that is not in the catalog is skipped without a cooldown. A
|
|
131
|
+
transient failure (rate limit, timeout, an upstream provider error behind a
|
|
132
|
+
router) is retried once on the same model, then cooled down for three minutes
|
|
133
|
+
— a router like `openrouter/free` picks a provider per request, so the retry
|
|
134
|
+
usually lands somewhere else. A deterministic failure (bad key, rejected
|
|
135
|
+
payload) is cooled for the full `cooldownSeconds` and the chain moves on to
|
|
136
|
+
the next candidate.
|
|
137
|
+
- When a stage ends with no model at all, the session gets one notice with the
|
|
138
|
+
last error, so a busy upstream is not mistaken for a missing key.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
Plugin options in `opencode.json` win over a project file
|
|
145
|
+
(`.opencode/blackhole.json`), which wins over the global file
|
|
146
|
+
(`~/.config/opencode/blackhole.json`), which wins over the defaults.
|
|
147
|
+
|
|
148
|
+
```jsonc
|
|
149
|
+
{
|
|
150
|
+
"plugins": [
|
|
151
|
+
{
|
|
152
|
+
"package": "opencode-blackhole",
|
|
153
|
+
"options": {
|
|
154
|
+
"enabled": true,
|
|
155
|
+
"compaction": {
|
|
156
|
+
"enabled": true,
|
|
157
|
+
"brief": false, // keep the ranked transcript tail (duplicates OpenCode's kept tail)
|
|
158
|
+
"briefTokens": 2000, // token budget for the brief, when enabled
|
|
159
|
+
"maxSummaryTokens": 4000, // hard cap for the whole summary
|
|
160
|
+
"extraNote": "", // extra text appended to every summary
|
|
161
|
+
"handoff": true, // one model call for the narrative block
|
|
162
|
+
"handoffModel": { "providerID": "openrouter", "id": "openrouter/free" },
|
|
163
|
+
"handoffFallbacks": [],
|
|
164
|
+
"handoffMaxTokens": 900, // budget for the block itself
|
|
165
|
+
"handoffInputMaxTokens": 12000, // how much conversation the prompt carries
|
|
166
|
+
"handoffTimeoutSeconds": 120 // past this, keep the deterministic summary
|
|
167
|
+
},
|
|
168
|
+
"memory": {
|
|
169
|
+
"enabled": true,
|
|
170
|
+
"observerModel": { "providerID": "openrouter", "id": "openrouter/free" },
|
|
171
|
+
"reflectorModel": { "providerID": "openrouter", "id": "openrouter/free" },
|
|
172
|
+
"dropperModel": { "providerID": "openrouter", "id": "openrouter/free" },
|
|
173
|
+
"observerFallbacks": [],
|
|
174
|
+
"reflectorFallbacks": [],
|
|
175
|
+
"dropperFallbacks": [], "sessionFallback": true, // the session's model is the last resort
|
|
176
|
+
"observeAfterTokens": 15000,
|
|
177
|
+
"reflectAfterTokens": 15000, // upstream uses 25000; see the notes below
|
|
178
|
+
"observerChunkMaxTokens": 40000, // the most the observer sees at once
|
|
179
|
+
"observerRefine": true, // a second pass reviews the observer's draft
|
|
180
|
+
"observationsPoolMaxTokens": 20000,
|
|
181
|
+
"retryGateSeconds": 30,
|
|
182
|
+
"cooldownSeconds": 3600
|
|
183
|
+
},
|
|
184
|
+
"recall": {
|
|
185
|
+
"enabled": true,
|
|
186
|
+
"responseMaxChars": 48000 // 0 = unbounded
|
|
187
|
+
},
|
|
188
|
+
"retention": {
|
|
189
|
+
"transcriptDays": 7, // 0 = keep forever
|
|
190
|
+
"memoryDays": 90, // the ledger holds the session's memory
|
|
191
|
+
"logDays": 7
|
|
192
|
+
},
|
|
193
|
+
"limits": {
|
|
194
|
+
"maxCachedSessions": 8, // parsed sessions kept in memory
|
|
195
|
+
"maxMessageChars": 262144, // cap on a single stored message
|
|
196
|
+
"maxLedgerBytes": 65536 // a ledger is compacted past this
|
|
197
|
+
},
|
|
198
|
+
"logLevel": "off" // "info" | "debug" writes a JSONL trace
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Worker model resolution:** `observerModel` → `observerFallbacks[]` → (the
|
|
206
|
+
session's model, because `sessionFallback` defaults to true, as upstream does).
|
|
207
|
+
A model that fails gets a persisted cooldown and is skipped on later attempts; a
|
|
208
|
+
stage that cannot run is skipped, and the coverage cursor never advances past
|
|
209
|
+
what a worker actually saw — so nothing is lost, it is just observed later.
|
|
210
|
+
|
|
211
|
+
The observer is shown at most `observerChunkMaxTokens` of the pending window at
|
|
212
|
+
a time (upstream's limit). The chunk it reads is covered; the rest stays
|
|
213
|
+
pending for the next round, and an answer the plugin cannot parse leaves the
|
|
214
|
+
window pending rather than discarding it. The reflector keeps its own cursor:
|
|
215
|
+
each observation is crystallised once, so repeated runs cannot pile up
|
|
216
|
+
duplicate reflections.
|
|
217
|
+
|
|
218
|
+
**The observer runs two passes** (`observerRefine`, on by default). The first
|
|
219
|
+
extracts a draft; the second sees that draft next to the same transcript and
|
|
220
|
+
returns the corrected list — the facts the first pass missed, the ones it got
|
|
221
|
+
wrong, the ones that were never durable. Upstream gets the same effect from
|
|
222
|
+
its agent loop, which reads its own first batch back; OpenCode's plugin API
|
|
223
|
+
has no tool loop, so the review is a second call instead. A review that fails
|
|
224
|
+
or comes back empty keeps the draft, so the second pass can only improve
|
|
225
|
+
things. It costs one more request per chunk against your worker model's
|
|
226
|
+
limits — turn it off with `"observerRefine": false` to go back to one call.
|
|
227
|
+
|
|
228
|
+
`reflectAfterTokens` is 15,000 rather than upstream's 25,000 because this
|
|
229
|
+
port's observation pool is a hard delete at 20,000, while upstream's is a
|
|
230
|
+
budget on the projection: at 25,000 the reflector could never fire here.
|
|
231
|
+
|
|
232
|
+
Set `"sessionFallback": false` if you would rather the workers stop than run on
|
|
233
|
+
your session's model: then a worker with no reachable model is skipped and the
|
|
234
|
+
session says so once.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Commands and tools
|
|
239
|
+
|
|
240
|
+
| Command | Description |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `/blackhole` | Compact now with blackhole's checkpoint (no summarizer call) |
|
|
243
|
+
| `/blackhole-recall <query>` | Search this session's history (`page:N`, `scope:all`, `#N:path`) |
|
|
244
|
+
| `/blackhole-memory` | Show the memory status: observations, reflections, pending tokens |
|
|
245
|
+
| `/blackhole-export [out:<path>.md]` | Export the session's memory as markdown |
|
|
246
|
+
|
|
247
|
+
The agent also gets a `blackhole_recall` tool: keyword/regex search, entry
|
|
248
|
+
ranges, full-text expansion of a `#N` reference, and the list of files worked on.
|
|
249
|
+
`#N` is the same index that appears in the summary refs (`(#12)`).
|
|
250
|
+
|
|
251
|
+
A recall response is bounded by `recall.responseMaxChars` (48,000 by default):
|
|
252
|
+
the budget is shared across the entries of a response, and each entry's lines
|
|
253
|
+
share their slice of it, so one huge stored message cannot flood the context it
|
|
254
|
+
was meant to help with. Set it to `0` for unbounded output.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## How the compaction is replaced
|
|
259
|
+
|
|
260
|
+
OpenCode's compaction hook fires for automatic compaction, overflow recovery and
|
|
261
|
+
`/compact`. The plugin answers it with a summary, and OpenCode skips its own
|
|
262
|
+
summarizer call — the compaction message then carries a zero-usage summary plus
|
|
263
|
+
OpenCode's own verbatim tail.
|
|
264
|
+
|
|
265
|
+
The summary has two halves. The **sections** are extracted from the transcript
|
|
266
|
+
without a model: session goal, files read/modified/created, commits, user
|
|
267
|
+
preferences, outstanding context. They hold exact paths and identifiers but say
|
|
268
|
+
nothing about *why* a thing was done. The **handoff block** is one model call for
|
|
269
|
+
exactly that gap — `[Decisions]`, `[Work State]`, `[Next Move]`,
|
|
270
|
+
`[Important Context]` — placed after the sections and before the recall note.
|
|
271
|
+
|
|
272
|
+
The block is additive: a failed, slow (past `handoffTimeoutSeconds`) or
|
|
273
|
+
unparseable response leaves the deterministic summary untouched. When the model
|
|
274
|
+
is unreachable, a smaller block is derived without one: the work state from the
|
|
275
|
+
commits, the modified-file count and the last command, plus the question the
|
|
276
|
+
session ended on. It never guesses a decision. Turn the whole thing off with
|
|
277
|
+
`"handoff": false` to get the purely deterministic summary.
|
|
278
|
+
|
|
279
|
+
The brief transcript (`"brief": true`) is off by default: OpenCode already keeps
|
|
280
|
+
the newest exchange verbatim, so the brief mostly duplicates it. It remains
|
|
281
|
+
useful if you compact very long sessions on a model with a small kept tail.
|
|
282
|
+
|
|
283
|
+
If the extraction produces nothing usable, the plugin leaves the hook result
|
|
284
|
+
unset and OpenCode falls back to its native summarizer. A compaction never
|
|
285
|
+
happens without a summary.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Known differences from pi-blackhole
|
|
290
|
+
|
|
291
|
+
* **Tail policy** is OpenCode's: the newest exchange is kept verbatim by the
|
|
292
|
+
core (`compaction.keep.tokens`), and the plugin's summary covers the rest.
|
|
293
|
+
* **Provider-native compaction** (Claude/GPT server-side context management)
|
|
294
|
+
bypasses the plugin hook entirely. Every other model — including everything
|
|
295
|
+
served through OpenRouter — goes through the deterministic path.
|
|
296
|
+
* **The dropper proposes, a deterministic rule decides.** Upstream's dropper
|
|
297
|
+
asks a model which observations are safe to remove, then ranks whatever it
|
|
298
|
+
proposed and caps the count. This port does the same: one call proposes the
|
|
299
|
+
ids, and the deterministic rule still decides which of them actually go —
|
|
300
|
+
least supported first, oldest within a tier. When the model cannot be
|
|
301
|
+
reached, the rule alone prunes the pool, so the pool always falls under its
|
|
302
|
+
cap.
|
|
303
|
+
* **Workers use one-shot structured calls.** OpenCode's plugin API has no
|
|
304
|
+
tool-calling loop, so the observer cannot refine its output over several
|
|
305
|
+
turns. It gets a second pass instead: the draft comes back for review
|
|
306
|
+
against the same transcript, and the corrected list replaces it
|
|
307
|
+
(`observerRefine`).
|
|
308
|
+
* **No interactive settings overlay** — the CLI plugin surface is out of scope.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## State on disk
|
|
313
|
+
|
|
314
|
+
The state lives **next to OpenCode's own data, in your home directory — not
|
|
315
|
+
inside your project**:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
~/.local/share/opencode/blackhole/
|
|
319
|
+
<projectID>/
|
|
320
|
+
transcripts/<sessionID>.jsonl # the session transcript (for recall)
|
|
321
|
+
memory/<sessionID>.jsonl # observations, reflections, coverage cursor
|
|
322
|
+
cooldowns.json # worker model cooldowns
|
|
323
|
+
blackhole.log # trace (logLevel: debug)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
A project's folder is keyed by OpenCode's `projectID`, not by its path, so
|
|
327
|
+
moving or renaming the project keeps its history. Your repository is never
|
|
328
|
+
written to — there is nothing to add to `.gitignore`, and no conversation
|
|
329
|
+
content is synced to a cloud drive with your code.
|
|
330
|
+
|
|
331
|
+
**Nothing grows without bound.** Transcripts expire after `retention.transcriptDays`
|
|
332
|
+
(7 by default) — they duplicate what OpenCode already stores in the session
|
|
333
|
+
record, so only the searchable index is lost. Ledgers hold the session's actual
|
|
334
|
+
memory, so they are kept much longer (`retention.memoryDays`, 90 by default)
|
|
335
|
+
and are compacted in place once they pass `limits.maxLedgerBytes`: the live
|
|
336
|
+
observations, the reflections and the cursor are rewritten, and the dropped
|
|
337
|
+
markers are folded into a counter. At most `limits.maxCachedSessions` sessions
|
|
338
|
+
are held in memory at once, and a single stored message is clipped to
|
|
339
|
+
`limits.maxMessageChars`.
|
|
340
|
+
|
|
341
|
+
**Privacy:** the transcript stores the full text of your tool output on disk,
|
|
342
|
+
locally, in your home directory. If you paste secrets into a session, they end
|
|
343
|
+
up in that file until it expires. Use `retention.transcriptDays: 0` to keep
|
|
344
|
+
them forever, or a short window to keep them less.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Development
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
bun install
|
|
352
|
+
bun run check # typecheck + tests + build
|
|
353
|
+
bun run dev # rebuild the bundle on every source change
|
|
354
|
+
bun run bench # where the time goes in the capture path
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**The plugin loads the bundle, not the sources.** A change under `src/` only
|
|
358
|
+
reaches a running OpenCode after `bun run build` (or `bun run dev`, which does
|
|
359
|
+
it for you); OpenCode then reloads the plugin by itself — no restart needed.
|
|
360
|
+
Changing the root `index.ts` takes effect immediately, since it is part of the
|
|
361
|
+
plugin's own file graph.
|
|
362
|
+
|
|
363
|
+
To try a local build in OpenCode, point a plugin file at it:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
// ~/.config/opencode/plugins/blackhole.ts
|
|
367
|
+
export { default } from "/path/to/opencode-blackhole/index.ts"
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The compaction engine lives in `src/vcc/` — a port of pi-vcc with the Pi
|
|
371
|
+
couplings removed, kept as a separate module so upstream fixes can be merged
|
|
372
|
+
rather than re-implemented. `LICENSE.vendor` carries its MIT license, and
|
|
373
|
+
`src/vcc/README.md` records exactly what diverges from upstream.
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
MIT. Ported from [pi-vcc](https://github.com/sting8k/pi-vcc) and
|
|
378
|
+
[pi-blackhole](https://github.com/k0valik/pi-blackhole) (both MIT).
|