pi-blackhole 0.5.9 → 0.5.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## [0.5.11] - 2026-10-05
6
+
7
+ ### Added
8
+
9
+ - **Oversized work shrinks to fit instead of stalling.** When no configured model can hold the sized input, the stage re-plans against the largest resolved window and runs that: the observer caps its chunk and drains the remainder in-run, the reflector splits new items into batches that all complete before the single cursor advance, and the dropper evaluates batches under pool-wide pressure and applies the ranker + cap once. A failed batch still voids its run with the cursor unmoved.
10
+ - **A warning when `observeAfterTokens` exceeds `observerChunkMaxTokens`.** Every pass then necessarily runs as multiple capped + drained batches. The loader warns (never clamps); skipped when `memory` is off.
11
+
12
+ ### Fixed
13
+
14
+ - **Observer coverage can no longer skip entries the model never saw.** The cap keeps the oldest contiguous prefix, coverage derives from the ids the serializer actually emitted, and after a recorded or clean-empty close the stage drains the remaining backlog in the same run — a delay, never a loss.
15
+ - **Cut-off worker runs no longer count as finished.** `length`, `aborted`, an unexempt terminal `toolUse` — or any other non-`stop` reason — now fail instead of advancing coverage over work that never happened. A run that stopped on its own stays success even if the signal aborted in the settle window, a length cut on a capped turn still reports as the turn cap, and a throwing stream function surfaces as an ordinary worker error instead of hanging the run.
16
+ - **Small-context models run instead of silently never running.** The fit check prices the measured prompt (content + summaries + static overhead + turn headroom, all CJK-aware) against window minus output allowance minus margin; the output allowance is capped at a quarter of the window, and a too-small window skips without an hour-long cooldown. The stage dispatches that same allowance as the workers' generation cap, so a fitting prompt cannot overflow at generation time.
17
+ - **Compaction no longer freezes on long encoded tool output.** Searching tool results for file paths could spend minutes in a regular expression when output contained a large encoded value, leaving both automatic compaction and `/blackhole` unresponsive. Path scanning now avoids repeated scans and overlapping repetitions while preserving file matching for error retries, and is additionally bounded (head+tail window with a token cap) so multi-megabyte results stay cheap ([#142](https://github.com/k0valik/pi-blackhole/pull/142)).
18
+
19
+ - **The footer stops promising an observer pass that cannot run.** With `memory: false` the O gauge still filled past `observeAfterTokens` and turned warning/error colored, while `consolidation.ts` hard-returns before launching an observer — in a ~300k-token session it read as "observer full but never fired". `status-bar.ts` now gates rendering on `memory` as well as `statusBar`: O and P are omitted whenever memory is off, so nothing can render as if a note-taking pass were due. The gate is read on every render, so `/blackhole om-off`, `/blackhole om-on`, and settings saves take effect within the bar's 1-second poll (gauges return as soon as memory is re-enabled), the X compaction gauge and worker events are unchanged, and `statusBar: false` / `ctx.hasUI === false` still suppress the whole bar.
20
+ - **"Display all settings" no longer credits a tab's value to the layer that overrides it.** A non-winning tab printed `(from <winner>)` next to its own value, so the Global row `on (from Project Local)` claimed Project Local supplied `on` while it actually set `off` — the opposite of the precedence that applied. A higher-precedence winner is now labelled `overridden by <layer>`, and `(from <layer>)` survives only where a lower layer genuinely supplies the value on show; the winning tab still reads `▸ effective`, and layer order, tab order, and the Global-first opening tab are untouched. Edit mode uses the same rule (shared helper), so the scope-locked view no longer prints the old `(from …)` claim for the same key.
21
+
22
+ ---
23
+
24
+ ## [0.5.10] - 2026-09-29
25
+
26
+ ### Fixed
27
+
28
+ - **`/blackhole` on a branch Pi considers ineligible now reports nothing-to-compact instead of failing.** Pi's `AgentSession.compact()` runs `prepareCompaction()` _before_ it emits `session_before_compact`, so a branch whose entries after the last boundary fit inside `keepRecentTokens` is refused by the host before any blackhole hook can run. The refusal surfaced twice — as Pi's error card (`reason: "manual"` always renders `Compaction failed: …`) and again as `Compaction failed: Nothing to compact (session too small)` from the command's `onError`, whose benign set covered only `Compaction cancelled` and `Already compacted`. The 0.5.4 eligibility fix gated the auto-compaction trigger but not this call site. The command now asks the host's own question via the new `getCompactionIneligibility()` before it mutates anything: an ineligible branch reports `blackhole: nothing to compact yet — Pi's keep-recent budget still covers this branch` (or `already compacted — …`) as information and returns, and the check sits above the pending-observational-memory flush so a refusal no longer appends entries, clears the pending buffer, and announces a flush that never compacted. `onError` keeps the same messages as a backstop for the case where our probe and the host still disagree.
29
+ - **The eligibility probe now resolves compaction settings with the session model.** `isCompactionEligible` called `settingsManager.getCompactionSettings()` with no argument, while Pi calls `getCompactionSettings(model)`; Pi resolves `compaction.modelOverrides["<provider>/<id>"]` for `keepRecentTokens` and `reserveTokens` only when handed the model, so the probe was asking a different question than the host — with a per-model override configured it could report a branch eligible that `compact()` then refuses (or the reverse). All four call sites now pass `ctx.model`.
30
+ - **A cancelled `/blackhole` no longer adds a second, vaguer toast.** The own-cut guard already names the specific reason (`blackhole: Nothing to compact (no live messages)`, or the `tailBehavior` hint) before returning `{ cancel: true }`, and Pi renders `Compaction cancelled` itself for manual aborts; the command's `onError` was adding a third message on top of those.
31
+ - **`/blackhole` loads the config before reading it.** The handler read `runtime.config.compaction` to decide whether to flush pending observational memory, but never called `runtime.ensureConfig`, which only `agent_start` did. When `/blackhole` was the first command of a session, a configured `compaction: "manual"` install read `DEFAULTS` (`"auto"`) and silently skipped the flush, so memory accumulated in the pending file and the summary missed it.
32
+ - **An observer stream error no longer counts as a completed chunk.** A provider failure after a recorded batch used to return partial work as success, advancing `coversUpToId` past content that was never observed. `runObserver` now throws a `WorkerStreamError` (message unchanged); the stage keeps the cursor and retries through the fallback chain, logging `discardedCount`. A valid `complete: true` close from a run that recorded something is still kept when a later turn fails: the failure is warned with the error class only (per [#80](https://github.com/k0valik/pi-blackhole/issues/80)), logged as `observer.error_after_close`, and cools the model when deterministic. A run the turn cap cuts off before it closes is treated the same way when it recorded something; a cap firing before anything was recorded is still an empty success. ([#137](https://github.com/k0valik/pi-blackhole/pull/137))
33
+ - **A failed reflector or dropper run no longer counts as finished work.** Both stages advance their cursor on success, so a partial result marked that work covered: unreflected observations were never crystallized again, and half-evaluated drop candidates were written under an `OM_OBSERVATIONS_DROPPED` marker. `runReflector` now throws a `WorkerStreamError` unless a valid `complete: true` batch that recorded reflections closed the review (a kept close returns `errorAfterClose`, which the stage cools and warns about exactly like the observer's); `runDropper` always throws, since `drop_observations` has no complete flag to prove the evaluation finished. The turn cap cuts a run that recorded something the same way an error does (a cap before anything was recorded is still an empty success), both stages log `discardedCount`, and neither spends another `agentMaxTurns` budget retrying a session model the cap already exhausted. The cap only reports exhaustion for a turn that did tool work, so a run that stopped naturally on exactly its last allowed turn keeps its result. ([#137](https://github.com/k0valik/pi-blackhole/pull/137))
34
+ - **A stream failure that never reaches `agent_end` still reports what it discarded.** When the stream itself breaks, or `result()` rejects, the observer, reflector and dropper rethrow that error with the count of records already written attached, so the stage error logs no longer show `discardedCount: undefined` for the most common way a run dies. The error keeps its own identity — wrapping it would break the stage's stale-context and timeout checks — and a thrown value that cannot carry a property simply loses the count rather than the failure. A run cut off by the hard `workerAttemptTimeoutMs` deadline is the deliberate exception: the deadline rejects immediately with its own error instead of waiting to learn what the worker had recorded, so that path still logs `discardedCount: undefined`. ([#137](https://github.com/k0valik/pi-blackhole/pull/137))
35
+
36
+ - **A provider body no longer cools a model because the worker framed it as an error.** `isDeterministicError` only accepts a bare 4xx code alongside an error signal word, but the `<worker> API error: ` framing supplied that word on every message the observer, reflector and dropper emit — so a status-shaped token anywhere in a provider body (`processed 401 rows`) counted as a real status and cooled the session model for the default hour. The framing is stripped before the signal-word check, the three workers build their messages from one prefix shared by the throw path and the kept-close path so the two cannot drift, and the reflector's kept-close debug entry now classifies the same framed message it would throw. ([#137](https://github.com/k0valik/pi-blackhole/pull/137))
37
+ - **The kept-close toast no longer claims a cooldown that was not recorded.** A session model whose resolved config carries no provider/id never reaches the cooldown file (`recordDeterministicError` keys it on both), yet the warning still said `model cooled down; details in cooldown log`. It now reports only what was written (`deterministic error; no cooldown recorded`), alongside the existing `cooldownHours: 0` branch, which the observer and reflector stages share through one helper. ([#137](https://github.com/k0valik/pi-blackhole/pull/137))
38
+ - **Token counts no longer read as retryable provider statuses.** The numeric alternatives in `RETRYABLE_ERROR_RE` matched as substrings, so `1500 tokens`, `5000 rows`, or `14290` classified retryable on the `retryable:`/`cooldownWorthy:` debug axis. The codes (`429`, `500`, `502`–`504`) are now word-boundary anchored; standalone codes and real phrasing (`429 Too Many Requests`, `HTTP 503`) still match. Nothing branches on this axis today (candidate cooldowns are recorded unconditionally), so the change is log-precision only.
39
+
40
+ ---
41
+
5
42
  ## [0.5.9] - 2026-09-26
6
43
 
7
44
  ### Added
package/README.md CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  **Deterministic compaction + session-aware observational memory for [Pi](https://github.com/earendil-works/pi) — in one unified extension.**
4
4
 
5
- `/blackhole` replaces Pi's LLM-based `/compact` with an algorithmic structural summary — fast, zero-cost. Three background workers (Observer, Reflector, Dropper) capture durable facts and decisions that survive across compactions. Per-worker model fallback chains with persisted cooldowns. Manual flush mode. One JSON file to configure it all.
5
+ `/blackhole` replaces Pi's LLM-based `/compact` with an algorithmic structural summary — fast, and the compaction step itself is zero-cost. Three background workers (Observer, Reflector, Dropper) run as separate billed model calls to capture durable facts and decisions that survive across compactions. Per-worker model fallback chains with persisted cooldowns. Manual flush mode. One JSON file to configure it all.
6
+
7
+ > **A note from the maintainer: this is experimental.**
8
+ >
9
+ > It won't save you tokens by itself. Compacting is free — no model call, just structure. Remembering is not: the background workers wake up as your session grows, read what happened, and write down what seems worth keeping. Those are real billed calls. Untuned, on your main coding model, this costs more than doing nothing. It only earns its keep with the workers on something cheap or free.
10
+ >
11
+ > It also doesn't solve compaction — nobody has, as far as I know. It takes a different approach ([observational memory](https://mastra.ai/blog/observational-memory), via [pi-observational-memory](https://github.com/elpapi42/pi-observational-memory)) and merges it with [pi-vcc](https://github.com/sting8k/pi-vcc), because the two conflicted when installed together. I merged them and vibeslopped a lot on top: robustness work, new behavior, and bugfixes ported from both upstreams. The recap is pattern-matching, not understanding: it guesses at goals and preferences from their shape. The memory is model-written, so it has the opposite weakness: it can keep what sounds right but isn't. Assume both need the search tool as a backstop.
12
+ >
13
+ > Want only one half? Use the project that does just that half. No hard feelings.
14
+ >
15
+ > Scope stays tight on purpose: forks welcome, bug fixes welcome and reviewed, new features only if they serve conversation compaction itself — not the week's AI fad bolted on. Almost everything is a toggle, which I'll admit is tiring; I use a small slice myself. Memory on most days, off some days. Workers on free models, waking rarely, size-based auto-compaction as a backstop. Most days I don't compact at all — fresh sessions beat resumed ones.
16
+ >
17
+ > Best fit is someone who doesn't count every token: code locally and push the workers to free hosted models, or code on a top model and run the workers on a local card — knowing each worker's different instructions wipe the local prompt cache for the next one. No measurements, no comparisons against other methods. Fast and nearly free the way I run it, good enough for what I need.
6
18
 
7
19
  ---
8
20
 
@@ -34,14 +46,13 @@ Then `/reload` or restart Pi. The config file at `~/.pi/agent/pi-blackhole/pi-bl
34
46
 
35
47
  ## ✨ What's new
36
48
 
37
- > **Latest release: [0.5.9](CHANGELOG.md)**
49
+ > **Latest release: [0.5.11](CHANGELOG.md)**
38
50
  >
39
- > - **Recall drill-downs can no longer flood your context** — every `#N:path` / `#N:text` expansion is capped by `recallResponseMaxChars`, and the cap names where to pick up (`continue at #42:src/auth.ts:412:30`), so a minified bundle or a huge write no longer dumps itself into the prompt.
40
- > - **Quiet sessions** — `showWorkerNotifications: false` silences routine observer/reflector/dropper progress toasts; warnings, errors, and `/blackhole*` command output stay visible.
41
- > - **Worker prompt caching is tunable per install** — `cacheRetention: none | short | long` is forwarded to all three worker streams, and unset inherits Pi's effective setting.
42
- > - **A hung worker model fails on a clock, not a hope** — new `workerAttemptTimeoutMs` bounds the whole agent loop (header waits, heartbeats, tool turns) per attempt; configured candidates then enter their normal cooldown and fallback chain.
43
- > - **Finished worker runs stop early** — `record_observations` / `record_reflections` accept a `complete` flag, so a fully-processed run ends instead of spending its `agentMaxTurns` budget.
44
- > - **Dropper pool pressure now works end to end** — gated on `observationsPoolMaxTokens` (the same pool the footer P gauge shows), it bypasses new-data gates and `1.0` disables it.
51
+ > - **Oversized memory work shrinks to fit instead of stalling** — when no configured model holds the sized input, the stages re-plan against the largest window and run, so small-context models work instead of silently never running.
52
+ > - **The observer can no longer skip entries it never saw** — the chunk cap keeps the oldest prefix and drains the backlog in-run, so coverage never advances past content the model wasn't shown.
53
+ > - **Cut-off worker runs fail into fallback instead of counting as finished** — `length`/`aborted`/stray tool-use endings no longer mark uncovered work done.
54
+ > - **Compaction no longer freezes on long encoded tool output** — bounded path scanning keeps multi-megabyte tool results cheap.
55
+ > - **A warning when `observeAfterTokens` exceeds `observerChunkMaxTokens`** — the loader warns (never clamps) that every pass will run as multiple batches.
45
56
  >
46
57
  > See [`CHANGELOG.md`](CHANGELOG.md) for the full history.
47
58
 
@@ -158,7 +169,7 @@ Defaults target ~128k context models and work out of the box — no tuning requi
158
169
  }
159
170
  ```
160
171
 
161
- Fallbacks (optional): each worker tries `stageModel → stageFallbacks → base model → session model` (skipping cooled-down models). By default the workers **do not** fall back to your session model — this avoids surprise cost and cache busting. Enable it with `sessionFallback: true` (default) or set `model` as a shared fallback. See [`docs/CONFIG.md` → Model Configuration](docs/CONFIG.md#model-configuration).
172
+ Fallbacks (optional): each worker tries `stageModel → stageFallbacks → base model → session model` (skipping cooled-down models). By default the session model **is** the last-resort fallback (`sessionFallback: true`), so workers keep running even with no worker models configured. If you don't want worker runs to touch your session model (extra cost and prompt-cache busting), configure cheap worker models (above) and set `sessionFallback: false` — or set `model` as a shared fallback so the session model is never reached. See [`docs/CONFIG.md` → Model Configuration](docs/CONFIG.md#model-configuration).
162
173
 
163
174
  Config file: **`~/.pi/agent/pi-blackhole/pi-blackhole-config.json`**
164
175
 
@@ -180,7 +191,7 @@ https://github.com/user-attachments/assets/a7dd804d-6aca-4bdb-8b6e-0dd779363a43
180
191
 
181
192
  Three background workers (separate LLM calls) run automatically during the session when `memory: true` (the default):
182
193
 
183
- - **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.
194
+ - **Observer** — reads conversation since the last observation marker and extracts timestamped facts: events, decisions, preferences. Input is capped to `observerChunkMaxTokens` as an oldest-first prefix (overflow drains in bounded follow-up batches) to prevent context blowup on long sessions — the coverage cursor never advances past entries the model was not shown. Runs most frequently.
184
195
  - **Reflector** — distills new observations into durable reflections: stable facts, patterns, and constraints that survive future compactions. Runs less often.
185
196
  - **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.
186
197