billion-context 0.1.165 → 0.1.166

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 CHANGED
@@ -42,6 +42,8 @@
42
42
 
43
43
  ---
44
44
 
45
+ > **Cache health at a glance:** a healthy session keeps a **95–97%** prefix-cache hit rate — compression itself costs ≤2%. Sustained lower? Check attribution with `/acp` or `/acp-cache` (see [FAQ](#faq)); usual causes, in order: upstream cache TTL expiry · model switch · a bili bug (please report) · other/unknown.
46
+
45
47
  ## Community
46
48
 
47
49
  QQ Group:
@@ -86,91 +88,14 @@ Agent (Claude Code / Codex / Cursor / Aider ...)
86
88
  real model API (Anthropic / OpenAI / compatible)
87
89
  ```
88
90
 
89
- The proxy injects four context-management tools (`compress`, `decompress`, `search_context`, `acp_status`) into the conversation. The model calls `compress` when the conversation grows, and the proxy executes it server-side — the compressed ranges are folded into the conversation history before the next turn.
90
-
91
- An opt-in fifth tool, `absorb` (`compress.absorb.enabled: true` — see [CONFIGURATION.md](CONFIGURATION.md)), compresses **individual tool results the moment they arrive**: large results (builds, logs, greps) get a forced absorb instruction, the model distills each into a compact summary, and the original pair is hidden from the wire from the next turn on — keeping mid-session pressure lower between fold rounds (#605).
92
-
93
- The sixth tool, `acp_rule` (opt-in via `compress.rules: true` — see [CONFIGURATION.md](CONFIGURATION.md); once enabled the model has full rights over session rules and may call it unprompted, #1399), records **persistent principle-level reminders**: a short rule recorded by the model (user-emphasized lessons, behaviors to remember, major pitfalls hit) is hard-protected from compression — the call and its result stay in context across every fold — and omitting the argument lists the recorded rules; passing `delete` with a rule id (e.g. `"rule3"`) removes one rule and `clear: true` removes all of them ([ranxianglei/billion-context-pi#433](https://github.com/ranxianglei/billion-context-pi/issues/433)).
94
-
95
- The seventh tool, `acp_retrieve` (opt-in on every lane — set `compress.ccr.enabled: true` at any level, after local verification; plugin lanes require the explicit global `true` so the manifest advertises the tool, #1271/#1273 — see [CONFIGURATION.md](CONFIGURATION.md)), backs the **content-addressed message store** (built-in CCR, #1097/#1179): oversized tool results are **ID-referenced at arrival instead of force-distilled** — the wire keeps a byte-stable placeholder and the original goes into a per-session content-store envelope (hash-deduped), retrievable on demand via one cheap tool call. V2 makes folds lossless too: covered originals are stored when a fold lands, `decompress` restores ranges (`startId`/`endId` refs) instead of whole blocks, and `search_context` hits carry the covered `mNNNNN` refs so you can fetch exactly what you need. Lossless by default: a retrieve not made costs nothing but the call; a detail distilled away by absorb is gone for good. Scope: proxy mode, plus plugin lanes on the anthropic + openai wires when explicitly enabled (`acp_retrieve` is advertised in the plugin manifest then, #1271); responses marker/text routes and google in plugin mode stay disarmed because no request-only round-trip channel exists there (silent loss, #1097).
96
-
97
- An opt-in tool, `image_full` (`compress.imageCompression.enabled: true` — see [CONFIGURATION.md](CONFIGURATION.md)), backs **image pre-compression** (#1095): screenshot-like images in tool results are downscaled once at arrival — the kernel decides routing and recipe, the host encodes via optional `sharp` — cutting billed pixels before they enter the wire (providers bill by pixel area; halving dimensions cuts billed tokens ~4×). Non-screenshot images pass through byte-identical. Lossy by nature: when the model can't read details it calls `image_full` with the message's ref to restore the original resolution for the rest of the session — no proxy-side storage needed, since the client's own history still carries the original bytes (it never saw the shrunk form). Default off.
91
+ ### Context-management tools
98
92
 
99
- A sibling protection knob, `compress.protectedLatestTools` (see [CONFIGURATION.md](CONFIGURATION.md)), keeps the **latest** snapshot of a cumulative tool (a client's todo/task list, e.g. `["todo_list", "TodoWrite"]`) un-compressible while older instances fold normally — so the agent never loses its live task list to a fold (#639). Its full-history counterpart `compress.protectedTools` hard-excludes **every** instance of a tool — for independent-content results no later result supersedes (e.g. opencode/pi `skill` loads); protecting all instances of a chatty or cumulative-snapshot tool grows context without bound (#639), so keep it to low-frequency, high-value tools. The inverse-direction knob `compress.neverPreserveRecentTools` (see [CONFIGURATION.md](CONFIGURATION.md), `acp-kernel` >= 0.0.92) removes tools from the soft-protected recent zone so their results fold immediately — by default only `decompress`/`search_context`/`read`/`bash` are exempt from recency; removing just `read` from that list is the recommended remedy for the batch-read fold→re-read death loop (#1198/#1277). Its positive-facing mirror `compress.preserveRecentTools` (see [CONFIGURATION.md](CONFIGURATION.md), `acp-kernel` >= 0.0.93) is the preferred one-entry form of that remedy — `{ "compress": { "preserveRecentTools": ["read"] } }` subtracts `read` from the effective exclusion list without restating or freezing the built-in default.
100
-
101
- ### Two compression modes — who executes `compress`
102
-
103
- The proxy runs in one of two modes, and **the mode decides who executes
104
- `compress`, which in turn decides how the summary travels to the model** (the
105
- "carrier"). This distinction is the root of #377.
106
-
107
- | | **Launcher / plugin mode** (`bili pi`, `bili codex`, …) | **Proxy mode** (plain client → `/bili/`) |
108
- |---|---|---|
109
- | Client | ACP-native agent with the bili extension (pi/omp) | Any OpenAI/Anthropic client, no extension |
110
- | Who executes `compress` | **The agent** (pi runs it locally) | **The proxy** (server-side compress loop) |
111
- | `compress` tool call in the re-sent history? | Yes — part of the agent's own conversation | No — ephemeral proxy-loop traffic |
112
- | Preflight blocks (no tool call)? | Last-resort backstop — the agent normally compresses on its own `compress` calls, but `src/preflight.ts` still fires (in both modes) when the input alone exceeds the window (#470) | Yes — `src/preflight.ts` compresses behind the client's back |
113
- | **Summary carrier on the wire** | **the `compress` tool call** | **an `acp_summary` user message** |
114
- | System messages on the wire | always exactly 1 (client + prompt) | always exactly 1 (client + prompt) — summaries ride on user messages |
115
- | SGLang "single system" 400 (#377) | cannot happen | cannot happen (summaries are user messages, not system) |
116
- | Proxy-injected `compress` tools | none — the agent registers the 4 ACP tools natively | the 4 context tools (when enabled) |
117
- | Proxy-injected nudge | **yes** — the agent has no nudge channel of its own, so the proxy-side nudge is the proactive compression trigger (preflight alone only fires at the hard limit; #451) | yes (when enabled) |
118
-
119
- **Why the carriers differ.** In plugin mode the agent owns compression: the
120
- `compress` call + result live in the agent's own history and are re-sent every
121
- turn, so the summary rides on the tool call and the agent's view never renders
122
- the kernel's `acp_summary` fallback (`billion-context-pi` `src/messages.ts`
123
- skips `acp_summary_*`). In proxy mode the client is not ACP-native, so the
124
- proxy executes `compress` server-side; the tool call never enters the client's
125
- history, and preflight blocks have no tool call at all — so the kernel's
126
- `acp_summary` message is the only carrier. The kernel renders it as role
127
- `system`, but strict OpenAI-compatible backends (SGLang) require exactly one
128
- system message at index 0, so `systemToUser` (`src/util.ts`) re-voices it as a
129
- `user` message, leaving it at its anchor position. This keeps the head system
130
- message (the prefix-cache anchor) byte-stable across compress turns, so a new
131
- block does not invalidate the whole-conversation prefix.
132
-
133
- **Why `user`, not `system` or a forged tool call.** A mid-stream `system`
134
- message is what SGLang rejects (#377). A forged `compress` tool call would be
135
- the "pure" carrier, but in proxy mode it requires fabricating an
136
- assistant `tool_calls` + `user` `tool_result` pair by id, declaring the tool in
137
- the request, and handling preflight blocks that have no authentic call — far
138
- more invasive than re-voicing a standalone note. A `user` message is allowed
139
- anywhere in the conversation, so it is the minimal change that satisfies both
140
- SGLang's one-system rule and prefix-cache stability. The accepted trade-off:
141
- a summary is a stand-in for the folded history, and re-voicing it as a user
142
- turn is a semantic mismatch the model tolerates (it is clearly marked
143
- `[Compressed conversation section]`).
144
-
145
- **Do the two modes coexist?**
146
-
147
- - **Same proxy instance: yes, by design.** One proxy serves plugin and plain
148
- clients at once; `pluginMode` is decided per request (`x-bili-plugin` header)
149
- and bound per session (`session.metadata.pluginAgent`). The launcher reuses a
150
- running proxy.
151
- - **Same session: the mode is sticky.** A session created in plugin mode stays
152
- plugin mode (metadata inheritance); a plain session can only be *upgraded* to
153
- plugin mode if a plugin request arrives with a matching conversation id (the
154
- header outranks) — and never downgraded. In practice a plain→plugin upgrade
155
- requires the plugin client's conversation id to match an existing plain
156
- session id, which doesn't happen (each client generates its own id).
157
- - **Cross-mode block hazard: theoretical only.** It would require the same
158
- conversation id to span a mode switch. plugin→proxy is safe (the tool call is
159
- in the shared history); proxy→plugin could orphan proxy-created block
160
- summaries (their tool call isn't in the agent's history and the agent's view
161
- skips `acp_summary`) — but that needs the id match above, which doesn't occur.
162
-
163
- **Verifying that a compression actually landed.** After executing `compress`,
164
- the proxy emits a confirmation marker (`📦 [ACP] Compressed …`) as plain
165
- assistant text — but under sustained context pressure a model was observed
166
- *writing that marker format itself* without ever calling the tool (#717): 17
167
- fake "compressions" over ~2 hours while real usage climbed to 89%. A marker
168
- line visible in the transcript is therefore not proof of persistence — verify
169
- with `acp_status` (block count increased, compressible-range start advanced)
170
- before trusting it. As a backstop, the proxy strips any marker-shaped line the
171
- model emits on its own and logs a `[marker-echo]` warning, and both the nudge
172
- and the injected prompt state explicitly that markers are proxy-emitted only.
93
+ The proxy injects four context-management tools into the conversation; the model calls them itself as context grows, and the proxy executes `compress` server-side so folded ranges stay summarized in history until restored:
173
94
 
95
+ - **`compress`** — fold a message range into a detailed summary.
96
+ - **`decompress`** — restore a compressed range when exact details are needed again.
97
+ - **`search_context`** — keyword search over compressed summaries and visible messages.
98
+ - **`acp_status`** — context-usage overview plus which ranges are still compressible.
174
99
 
175
100
  ## Which do I need?
176
101
 
@@ -179,25 +104,25 @@ Pick by your client:
179
104
  | Client | Use |
180
105
  |---|---|
181
106
  | **pi** | [`billion-context-pi`](https://github.com/ranxianglei/billion-context-pi) (in-process extension) |
182
- | **opencode** (1.x / 2.x) | [`billion-context`](https://github.com/ranxianglei/billion-context) — `bili opencode` (launcher) or `bili plugin install opencode` (native, no launcher); standalone [`opencode-acp`](https://github.com/ranxianglei/opencode-acp) remains usable on 1.x. Full guide: [OpenCode](#opencode) |
107
+ | **opencode** (1.x / 2.x) | [`billion-context`](https://github.com/ranxianglei/billion-context) — `bili opencode` (launcher) or `bili plugin install opencode` (native); standalone [`opencode-acp`](https://github.com/ranxianglei/opencode-acp) remains usable on 1.x — full guide: [OpenCode](CLIENTS.md#opencode) |
183
108
  | **omp** | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili omp` (built-in plugin) or `bili plugin install omp` (self-spawning native plugin, no launcher) |
184
- | **dsh** | `bili dsh` (launcher — full native plugin via `--patch`: tools, session-bound `/acp` + `/acp-cache`, fetch intercept) or `bili plugin install dsh` ≡ `dsh plugin --profile <name> add billion-context` (one unified lane — pnpm-installs the package into each profile so dsh mounts the bundled patch layer; the bili form just drives dsh's own channel per profile and migrates legacy managed blocks) |
185
- | **kimi** | `bili plugin install kimi` (self-spawning native plugin, no launcher — Kimi Code ≥ 2.0.0; per-session routing block in `~/.kimi-code/config.toml`) or `bili kimi` (launcher, cert-MITM) or `/bili/` prefix |
186
- | **hermes** | `bili plugin install hermes` (self-spawning native plugin, no launcher — Python plugin, #958) or `bili hermes` (launcher, cert-MITM) |
187
- | **zcode** (Z.ai / bigmodel coding plan) | `bili plugin install zcode` (self-spawning native plugin, no launcher — per-session routing block in the bigmodel provider store, #1145) or cert-MITM through the GUI's Settings → Network (HTTP proxy + CA path) or `/bili/` prefix |
109
+ | **dsh** | `bili dsh` (launcher — full native plugin via `--patch`) or `bili plugin install dsh` ≡ `dsh plugin --profile <name> add billion-context` (one unified lane) — details: [CLIENTS.md](CLIENTS.md) |
110
+ | **kimi** | `bili plugin install kimi` (self-spawning native, Kimi Code ≥ 2.0.0) or `bili kimi` (cert-MITM) or `/bili/` prefix — details: [CLIENTS.md](CLIENTS.md) |
111
+ | **hermes** | `bili plugin install hermes` (self-spawning native, Python plugin #958) or `bili hermes` (cert-MITM) |
112
+ | **zcode** (Z.ai / bigmodel coding plan) | `bili plugin install zcode` (self-spawning native, #1145) or cert-MITM via the GUI's Settings → Network or `/bili/` prefix — details: [CLIENTS.md](CLIENTS.md) |
188
113
  | **claude** | `bili claude` (launcher) or `bili plugin install claude` (native posture, #964 — managed settings block + session-owned proxy; see the notes below) |
189
- | **jcode** | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili jcode` (launcher, cert-MITM) or `/bili/` prefix — no native plugin possible: compiled Rust binary with no plugin seam, and its static per-provider config can't stamp per-request headers ([#962](https://github.com/ranxianglei/billion-context/issues/962)) |
190
- | **gemini** (Gemini CLI) | `bili gemini` (launcher, `GOOGLE_GEMINI_BASE_URL` `/bili/` rewrite) or `/bili/` prefix — launcher-only: gemini-cli's extension system reaches custom commands only, no in-loop tool seam (#1043) |
114
+ | **jcode** | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili jcode` (cert-MITM) or `/bili/` prefix — no native mode (compiled Rust binary, no plugin seam, [#962](https://github.com/ranxianglei/billion-context/issues/962)) |
115
+ | **gemini** (Gemini CLI) | `bili gemini` (launcher, `GOOGLE_GEMINI_BASE_URL` `/bili/` rewrite) or `/bili/` prefix — launcher-only (no in-loop tool seam, #1043) |
191
116
  | **iflow** (iFlow CLI) | `bili iflow` (launcher, `IFLOW_BASE_URL` `/bili/` rewrite) or `/bili/` prefix |
192
117
  | **qwen** (Qwen Code) | `bili qwen` (launcher, cert-MITM) or `/bili/` prefix |
193
- | **mcode** (MiniMax Code) | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili mcode` (launcher, cert-MITM) or `/bili/` prefix — no native plugin possible: its plugin system is declarative event hooks only (no model-request/history seam), so compression rides the proxy ([#1050](https://github.com/ranxianglei/billion-context/issues/1050)) |
194
- | **aider** | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili aider` (launcher, cert-MITM) or `/bili/` prefix — no native plugin possible: Python script structure whose hook surface is shell commands around edits/notifications only, no tool-injection seam ([#1048](https://github.com/ranxianglei/billion-context/issues/1048)) |
195
- | **copilot** (GitHub Copilot CLI) | `bili copilot` (launcher, cert-MITM) — closed Go binary, no plugin seam; model hosts (`api.githubcopilot.com` + per-plan subdomains) whitelisted (#1049) |
196
- | **amp** (Amp CLI) | `bili amp` (launcher, cert-MITM) — closed Go binary, no plugin seam; `ampcode.com` whitelisted (#1049) |
197
- | **goose** (Goose CLI) | `bili goose` (launcher) — rustls release builds trust no CA file, so no cert-MITM: built-in openai/anthropic legs redirected via `OPENAI_HOST`/`ANTHROPIC_HOST`, custom providers via a regenerated `GOOSE_PATH_ROOT` overlay (`base_url` → `/bili/`, real config untouched); fixed third-party providers unsupported (#1049) |
118
+ | **mcode** (MiniMax Code) | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili mcode` (cert-MITM) or `/bili/` prefix — no native mode (event hooks only, no model-request seam, [#1050](https://github.com/ranxianglei/billion-context/issues/1050)) |
119
+ | **aider** | [`billion-context`](https://github.com/ranxianglei/billion-context) via `bili aider` (cert-MITM) or `/bili/` prefix — no native mode (shell-command-only hooks, no tool-injection seam, [#1048](https://github.com/ranxianglei/billion-context/issues/1048)) |
120
+ | **copilot** (GitHub Copilot CLI) | `bili copilot` (launcher, cert-MITM) — closed Go binary, no plugin seam (#1049) |
121
+ | **amp** (Amp CLI) | `bili amp` (launcher, cert-MITM) — closed Go binary, no plugin seam (#1049) |
122
+ | **goose** (Goose CLI) | `bili goose` (launcher) — rustls trusts no CA file, so no cert-MITM: openai/anthropic legs via `OPENAI_HOST`/`ANTHROPIC_HOST`, custom providers via a regenerated `GOOSE_PATH_ROOT` overlay (#1049) |
198
123
  | **everything else** (no context hook) | [`billion-context`](https://github.com/ranxianglei/billion-context) — `bili <client>` (launcher, preferred) or `/bili/` prefix |
199
124
 
200
- **Native mode vs standalone extensions.** The host-native plugins (`bili plugin install pi` / `opencode` — they spawn the proxy inside the host process) and the standalone in-process extensions (`billion-context-pi`, `opencode-acp`) are **mutually exclusive**: both active means double compression. The installer makes the switch: `bili plugin install pi` replaces the legacy `npm:billion-context-pi` entry (with a reminder that a project-scope entry in `<project>/.pi/settings.json` from `pi install -l` lives outside the global settings), and `bili plugin install opencode` strips legacy `opencode-acp` entries from the global opencode.json — bare name, `npm:` alias, versioned (`opencode-acp@stable`), or path form, array or object shape; the original config is snapshotted to `.bili-bak` once. A **project-local** install (`opencode plugin opencode-acp` writes `<project>/.opencode/opencode.json`, not the global config) is not touched — remove it by hand; the installer note reminds you. As a runtime safety net for manual installs, the native entries set `BILLION_CONTEXT_NATIVE=<host>` synchronously at load so a standalone extension can stand down at action time — its own load-time `BILLION_CONTEXT_PROXY` check cannot see a proxy that native mode spawns asynchronously, and its `/bili/` baseUrl check never sees the fetch-layer rewrite. On the pi side the marker needs `billion-context-pi` **0.1.72+** (the per-event re-check landed after 0.1.71); the pi-native entry additionally scans both pi settings files once its proxy is up and warns loudly when it spots a co-resident legacy entry the installer never saw — that warning is the only visible signal while an old `billion-context-pi` silently double-compresses.
125
+ **Native mode vs standalone extensions.** The host-native plugins (`bili plugin install …`) and the standalone in-process extensions (`billion-context-pi`, `opencode-acp`) are **mutually exclusive** — both active means double compression. The installer makes the switch: it replaces the legacy entries (bare name, `npm:` alias, versioned, path form; array or object shape) and snapshots the original config to `.bili-bak` once; a **project-local** install is not touched — remove that one by hand. As a runtime safety net for manual installs, the native entries set `BILLION_CONTEXT_NATIVE=<host>` synchronously at load so a standalone extension can stand down at action time. On the pi side the marker needs `billion-context-pi` **0.1.72+**, and the pi-native entry scans both pi settings files once its proxy is up and warns loudly when it spots a co-resident legacy entry the installer never saw — that warning is the only visible signal while an old `billion-context-pi` silently double-compresses.
201
126
 
202
127
 
203
128
  ## Install
@@ -212,11 +137,11 @@ This installs the `bili` command (`bili-proxy` is kept as an alias).
212
137
 
213
138
  Three ways to use it — pick one:
214
139
 
215
- - **Native plugin (no launcher):** `bili plugin install <client>` — bili
140
+ - **Native plugin (most native):** `bili plugin install <client>` — bili
216
141
  becomes a plugin inside the client; start the client as usual.
217
- - **Launcher (easiest):** one `bili <client>` command brings up the proxy and
142
+ - **Launcher (config-free):** one `bili <client>` command brings up the proxy and
218
143
  the client together — no real config file is ever touched.
219
- - **URL change (persistent):** prefix your client's baseURL with the proxy
144
+ - **URL change (most universal):** prefix your client's baseURL with the proxy
220
145
  origin + `/bili/`.
221
146
 
222
147
  Mechanism details behind these three options (plugin lifecycle, runtime-info
@@ -238,7 +163,6 @@ bili plugin install kimi # writes $KIMI_CODE_HOME/plugins/managed/billion
238
163
  bili plugin install hermes # copies the Python plugin into ~/.hermes/plugins/billion-context/ (+ machine-owned bili.json sidecar) and enables it via `hermes plugins enable billion-context`
239
164
  bili plugin install zcode # writes hooks.enabled + a SessionStart hook + mcp.servers.bili into ~/.zcode/cli/config.json; per-session routing lands in the bigmodel provider store on first start
240
165
  bili plugin remove <client> # undo (dsh removes through the same channel; config snapshots go to .bili-bak)
241
- bili plugin update [client] # bring every lane's bili presence up to date, each through its own owner (see below)
242
166
  ```
243
167
 
244
168
  Where a client has its own plugin channel you can also install natively,
@@ -262,150 +186,17 @@ For pi / omp / kimi / claude there is no client-side channel — `bili plugin
262
186
  install <client>` writes their config entries for you (kimi's declarative
263
187
  `kimi.plugin.json` + registry record, claude's managed settings block, …).
264
188
 
265
- #### Single-writer: who owns which copy (#991)
266
-
267
- Every bili presence on a machine has exactly **one writer** — the thing
268
- that installed it is the thing that updates it, and nothing else ever
269
- overwrites that copy in place:
270
-
271
- | Lane | Copy lives in | Updated by |
272
- |------|---------------|------------|
273
- | global `bili` | npm global (`npm i -g billion-context`) | `bili update` / background auto-update |
274
- | **pi** | pi's package manager (npm form) | **`pi update`** — bili never overwrites it |
275
- | **opencode** | opencode's plugin dir | **opencode's plugin manager** — bili never overwrites it |
276
- | **dsh** | each profile's pnpm store | a periodic check re-runs dsh's plugin channel per profile — driven by the global bili self-update **or by the profile copy's own proxy** when the global isn't running (dsh-market installs, #1196); manual: `dsh plugin add billion-context@latest`. pnpm's hardlinked store must never be copied over in place |
277
- | omp / claude / codex / kimi / zcode | no copy — entries point at the global bili install | they update together with the global copy |
278
- | **hermes** | `~/.hermes/plugins/billion-context/` (copied files + `bili.json` sidecar pointing at the global dist) | **`bili plugin update hermes`** re-copies the files; the sidecar tracks the global install |
279
-
280
- This is enforced in code, not just convention: the self-updater
281
- (`src/update.ts` → `hostManagedInstall`) detects install dirs under a pnpm
282
- virtual store (`.pnpm`) or a host agent tree (pi / opencode / dsh / kimi /
283
- omp homes) and **skips** them; `installViaTarball` refuses them structurally
284
- so direct callers cannot corrupt a store either. Mixing *commands* is fine
285
- (`dsh plugin add` ≡ `bili plugin install dsh` — same channel, same records);
286
- mixing *writers* is what the guard forbids. `bili plugin update [client]`
287
- is the one command that drives every lane through its own owner and prints
288
- the per-lane update path (`bili plugin list` shows the same per-lane channel).
289
-
290
- At load the plugin **spawns its own proxy** (attaches to a healthy running
291
- one only when it passes the attach gate below; a parent-pid watchdog tears
292
- it down when the client exits),
293
- rewrites model traffic to `<proxy>/bili/<upstream-url>`, registers
294
- `compress` / `decompress` / `acp_status` as native client tools (plugin
295
- mode), and reports the client's **own model config** to the proxy so
296
- compression budgets use the real window instead of a registry guess.
297
- Opt-out envs: `BILI_NATIVE_PI=0`, `BILI_NATIVE_OMP=0`,
298
- `BILI_NATIVE_OPENCODE=0`, `BILI_NATIVE_DSH=0`, `BILI_NATIVE_KIMI=0`,
299
- `BILI_NATIVE_HERMES=0`, `BILI_NATIVE_ZCODE=0`. Full
300
- mechanics: [TECHNICAL-NOTES.md](TECHNICAL-NOTES.md).
301
-
302
- Reuse is identity-based (#1225) **and lifecycle-gated (#1335)**: an existing
303
- proxy is attached only when it runs the **same code** (sha256 of the entry
304
- script, recorded in the instance file), its **lane is compatible** — each
305
- launcher declares its client's lane, two *different declared* lanes never
306
- share — **and it owns a session lifecycle**: its health endpoint reports an
307
- armed parent-pid watchdog (`watchdog.armed == true`), i.e. it was spawned by
308
- a launcher with a parent pid and dies when the last attached session dies.
309
- An instance without a declared lane is wildcard-compatible on the lane axis,
310
- but that alone no longer makes it attachable (see the gate below).
311
- Instances written before #1225 carry no code fingerprint and are therefore
312
- never attached: a rebuilt or updated install always starts a fresh proxy on
313
- the next launch, so fixes take effect immediately instead of silently
314
- serving stale code.
315
-
316
- **The attach gate (#1335).** A native hook attaches to whatever answers on
317
- the port, so the three listener kinds get different treatment (TS lanes and
318
- the hermes Python plugin's discovery path alike, #1338):
319
-
320
- | Listener | Lifecycle owner | Attach? |
321
- |---|---|---|
322
- | Its own session-spawned proxy | armed from birth | ✅ yes |
323
- | Another session's armed proxy (shared, watcher set #1186) | watcher set | ✅ yes — sharing stays the design |
324
- | Manually started `bili start` daemon | **none** — refuses watchers, never dies with sessions, often an older build | ❌ not by default |
325
-
326
- The hook probes the candidate's `/__bili/health` for `watchdog.armed` before
327
- attaching. Armed → attach + register a watcher (current behavior, README
328
- lifecycle contract holds). Unarmed — or a pre-#1330 build that reports no
329
- `watchdog` field at all (unverifiable, treated as unarmed) → **do not
330
- attach**; the hook spawns its own session-owned proxy (ephemeral port, armed
331
- from birth, dies with the last session). This also fixes version skew: every
332
- session now runs the **currently installed** bili instead of whatever a
333
- stale resident daemon happens to carry. The trade-off is one extra short-lived
334
- proxy process per session when no armed proxy exists (session state is shared
335
- on disk, so compression continuity is unaffected); the multi-instance warning
336
- (#394) becomes correspondingly more common. **Escape hatch:** deliberately
337
- run a resident daemon for your hooks to ride on → set
338
- `native.attachExternal: true` in the config file or
339
- `BILI_NATIVE_ATTACH_EXTERNAL=1`. That restores attaching to any compatible
340
- listener regardless of watchdog state — you then own the daemon's lifetime
341
- and version yourself. Explicit user-directed attaches (`BILLION_CONTEXT_ATTACH`
342
- / preset `BILLION_CONTEXT_PROXY` for kimi/dsh) bypass discovery entirely and
343
- are exempt by construction.
344
-
345
- Attach discovery is lane-aware across **all** live instances (#1232): the
346
- launcher probes every live entry in the instance registry, not just the
347
- single instance file (last-writer-wins — under concurrent multi-client use
348
- it can point at another client's proxy), and applies the gate above to every
349
- candidate. Among compatible candidates the newest instance with the launcher's
350
- own declared lane wins; an instance without a lane is wildcard-compatible on
351
- the lane axis (still subject to the gate). The `another bili instance is
352
- running` warning (#394) is lane-aware too: it fires for same-lane or lane-less
353
- coexistence, but stays silent between two *different* declared lanes, whose
354
- session files are disjoint.
355
-
356
- **Runtime-info protocol (#955).** A native plugin reads the model config
357
- the client itself will use and pushes it to the proxy (per-request headers
358
- + bootstrap report); the proxy prefers that truth over the models.dev
359
- registry / built-in table when resolving the context window. Protocol
360
- details, resolution order, and implementations:
361
- [TECHNICAL-NOTES.md](TECHNICAL-NOTES.md).
362
-
363
189
  Notes:
364
190
 
365
- - Native mode is **mutually exclusive** with the standalone in-process
366
- extensions (`billion-context-pi`, `opencode-acp`) — the installer swaps
367
- the entries and snapshots the original config (`.bili-bak`); migration
368
- details in the client table above (pi needs `billion-context-pi` 0.1.72+
369
- to stand down cleanly).
370
- - OpenCode: legacy `opencode-acp` sessions, the V1/V2 plugin shapes, and all caveats are consolidated in the [OpenCode](#opencode) section.
371
- - `kimi` reports runtime-info at bootstrap only (static `custom_headers` can't
372
- carry per-request window/model headers without going stale on model switch)
373
- and binds subagent conversations by per-call `conversation_id` — full
374
- mechanics in the "Kimi Code" section below.
375
- - `hermes`'s native plugin is Python (its CLI agent's plugin API is
376
- Python-only) — instead of patching fetch it points hermes' httpx stack at
377
- the proxy via env vars after a health check, and stamps per-request headers
378
- through an `llm_request` middleware; full mechanics in the "Hermes" section
379
- below.
380
- - `codex` has a companion install too (an MCP shell), but it needs a running
381
- proxy — it is not native mode.
382
- - `claude` also has a **native posture** (#964): `bili plugin install
383
- claude` writes a managed settings block (static `/bili/` URL +
384
- `SessionStart` hook) plus an MCP shell pinned to a stable port — the
385
- proxy lives and dies with the session. Opt out with
386
- `BILI_NATIVE_CLAUDE=0` (passthrough). Mechanics:
387
- [TECHNICAL-NOTES.md](TECHNICAL-NOTES.md).
388
- - `zcode` also has a **native posture** (#1145): `bili plugin install
389
- zcode` writes `~/.zcode/cli/config.json` (`hooks.enabled` +
390
- `SessionStart` hook + stdio MCP server) and rewrites the bigmodel
391
- coding-plan provider's `baseURL` to `<proxy>/bili/<upstream>` per
392
- session (both store generations: legacy `v2/config.json` and v3.14+
393
- `provider_config.json`) — full mechanics in the "ZCode" section below.
394
- - `jcode` has no native mode at all: it is a compiled Rust binary with no
395
- plugin or extension seam, its only per-provider request surface is a static
396
- TOML header table applied verbatim to every request, and its MCP servers
397
- run in a global pool shared across all sessions — so there is neither a
398
- way to rewrite model traffic in-process nor one to stamp the per-request
399
- headers plugin mode requires (`x-bili-plugin`, conversation id,
400
- runtime-info). Full source-level analysis: [#962](https://github.com/ranxianglei/billion-context/issues/962)
401
- (closed wontfix). Use `bili jcode`.
402
- - `aider` has no native mode either: it is a Python script structure whose
403
- hook surface is limited to shell commands around file edits and idle
404
- notifications (`--git-commit-verify`, `--notifications-command`) — there is
405
- no plugin or extension API and no MCP client, so there is no
406
- tool-injection seam for plugin mode. Use `bili aider`
407
- ([#1048](https://github.com/ranxianglei/billion-context/issues/1048)).
408
- - `copilot`, `amp` and `goose` are launcher-only (#1049): none exposes a tool-injection seam, so there is no native mode (a codex-style MCP-shell companion remains possible for amp/goose but is not shipped). Goose additionally cannot be cert-MITMed — its release builds run rustls/webpki and trust no CA file — so it rides plain-HTTP base-URL redirects instead of proxy envs.
191
+ - Native mode is **mutually exclusive** with the standalone in-process extensions (`billion-context-pi`, `opencode-acp`) — the installer swaps the entries and snapshots the original config (`.bili-bak`).
192
+ - OpenCode legacy sessions, V1/V2 shapes and caveats: [OpenCode](CLIENTS.md#opencode).
193
+ - `kimi` reports runtime-info at bootstrap only (static headers can't carry per-request window/model values) and binds subagents by per-call `conversation_id`.
194
+ - `hermes`'s native plugin is Python: it points hermes' httpx stack at the proxy via env vars after a health check and stamps per-request headers through an `llm_request` middleware.
195
+ - `codex` has a companion MCP-shell install too, but it needs a running proxy — not native mode.
196
+ - `claude` has a native posture (#964): managed settings block + `SessionStart` hook + MCP shell on a stable port; opt out with `BILI_NATIVE_CLAUDE=0` (passthrough). Mechanics: [TECHNICAL-NOTES.md](TECHNICAL-NOTES.md).
197
+ - `zcode` has a native posture (#1145): managed `~/.zcode/cli/config.json` block + per-session provider `baseURL` rewrite. Full mechanics: [CLIENTS.md](CLIENTS.md).
198
+ - `jcode` and `aider` have no native mode (no plugin/MCP/tool-injection seam: #962, #1048) — use `bili jcode` / `bili aider`.
199
+ - `copilot`, `amp` and `goose` are launcher-only (#1049); goose cannot be cert-MITMed (rustls trusts no CA file) and rides plain-HTTP base-URL redirects instead.
409
200
 
410
201
  ### Option 2 — Launcher (`bili pi` / `bili codex` / `bili claude` / `bili omp` / `bili opencode` / `bili hermes` / `bili dsh` / `bili codebuddy` / `bili qoder` / `bili trae` / `bili jcode` / `bili kimi` / `bili gemini` / `bili iflow` / `bili qwen` / `bili mcode` / `bili aider` / `bili copilot` / `bili amp` / `bili goose`)
411
202
 
@@ -423,22 +214,22 @@ bili pi # launch pi through the proxy — file-fre
423
214
  bili codex # launch codex through the proxy
424
215
  bili claude # launch claude through the proxy
425
216
  bili omp # pi-style, file-free (#535): env + extension registerProvider + compaction cancel, real ~/.omp untouched
426
- bili opencode # OpenCode (1.x & 2.x): full guide in the [OpenCode](#opencode) section below
217
+ bili opencode # OpenCode (1.x & 2.x): full guide in the [OpenCode](CLIENTS.md#opencode) section below
427
218
  bili hermes # file-free (#535): hermes proxy env (HTTPS_PROXY + combined CA bundle via SSL_CERT_FILE) — https via CONNECT MITM, http via absolute-form forward proxy; real ~/.hermes untouched
428
- bili dsh # deepseek-harness: full native plugin injected via --patch (#941) — compress/decompress/acp_status registered as real dsh tools, requests stamped with the dsh session id (plugin mode), /acp + /acp-cache session-bound; non-loopback upstreams ride proxy envs (https MITM, http absolute-form), loopback keeps the overlay DSH_HOME (~/.dsh-bili) rewrite (#535), built-in deepseek route via DEEPSEEK_BASE_URL; dsh native auto-compaction disabled (compaction-basic auto:false)
219
+ bili dsh # deepseek-harness: full native plugin injected via --patch (#941) — real dsh tools, session-bound /acp + /acp-cache (plugin mode); non-loopback upstreams ride proxy envs, loopback keeps the overlay DSH_HOME rewrite (#535); dsh auto-compaction off
429
220
  bili codebuddy # Tencent CodeBuddy Code CLI: CODEBUDDY_BASE_URL /bili/ rewrite (OpenAI chat completions wire), budget aligned via CODEBUDDY_AUTO_COMPACT_WINDOW; real ~/.codebuddy untouched
430
221
  bili qoder # qoder: model endpoint is hardcoded https (no /bili/ rewrite possible) — cert-MITM via HTTPS_PROXY + NODE_EXTRA_CA_CERTS, default model hosts whitelisted (#653)
431
222
  bili trae # Trae CLI (ByteDance, closed Go binary, no base-URL override) — cert-MITM via HTTPS_PROXY + SSL_CERT_FILE, model host from TRAE_CLI_API_HOST or the default enterprise gateway (#655)
432
223
  bili jcode # jcode (Rust agent harness) — env-only cert-MITM launch: HTTPS_PROXY + SSL_CERT_FILE, model host api.z.ai whitelisted, local loopback providers stay direct via NO_PROXY
433
- bili kimi # Kimi Code CLI (Moonshot): honors standard proxy envs for all traffic EXCEPT an unconditional loopback bypass — non-loopback https via cert-MITM (HTTPS_PROXY + NODE_EXTRA_CA_CERTS/SSL_CERT_FILE), non-loopback http via absolute-form forward proxy; provider/model hosts from ~/.kimi-code/config.toml (KIMI_CODE_HOME respected) or the managed OAuth endpoints when none declared; loopback endpoints inventoried with a manual /bili/ prefix hint (#757)
224
+ bili kimi # Kimi Code CLI (Moonshot): standard proxy envs except an unconditional loopback bypass — https via cert-MITM, http via absolute-form forward proxy; loopback endpoints inventoried with a manual /bili/ hint (#757)
434
225
  bili gemini # Gemini CLI (Google): GOOGLE_GEMINI_BASE_URL /bili/ rewrite to generativelanguage.googleapis.com (Google native wire), real ~/.gemini untouched
435
226
  bili iflow # iFlow CLI: IFLOW_BASE_URL /bili/ rewrite to apis.iflow.cn/v1 (OpenAI chat-completions wire), real ~/.iflow untouched
436
227
  bili qwen # Qwen Code (multi-protocol gemini-cli fork, no base-URL hook): cert-MITM via HTTPS_PROXY + NODE_EXTRA_CA_CERTS, default DashScope/Qwen model hosts whitelisted, custom relays via --mitm-domain
437
- bili mcode # MiniMax Code CLI: honors standard proxy envs for all traffic EXCEPT an unconditional loopback bypass — non-loopback https via cert-MITM (HTTPS_PROXY + NODE_EXTRA_CA_CERTS/SSL_CERT_FILE), non-loopback http via absolute-form forward proxy; provider hosts from ~/.minimax*/config.yaml (MINIMAX_DATA_DIR/MAVIS_DATA_DIR respected) or the official agent.minimax.* endpoints when none declared; loopback endpoints inventoried with a manual /bili/ prefix hint; session bound via the X-Mavis-Session-Id header (#1050)
438
- bili aider # Aider (Python pair programmer): cert-MITM via HTTPS_PROXY + SSL_CERT_FILE/REQUESTS_CA_BUNDLE; endpoint from OPENAI_API_BASE / ANTHROPIC_BASE_URL etc., --openai-api-base, or .aider.conf.yml — api.openai.com + api.anthropic.com assumed by default; loopback endpoints stay direct via NO_PROXY (#1048)
228
+ bili mcode # MiniMax Code CLI: same shape as kimi (proxy envs, unconditional loopback bypass, cert-MITM/absolute-form); session bound via X-Mavis-Session-Id (#1050)
229
+ bili aider # Aider (Python pair programmer): cert-MITM via HTTPS_PROXY + SSL_CERT_FILE/REQUESTS_CA_BUNDLE; endpoint from OPENAI_API_BASE / ANTHROPIC_BASE_URL / --openai-api-base / .aider.conf.yml (#1048)
439
230
  bili copilot # Copilot CLI (GitHub, closed Go binary) — cert-MITM via HTTPS_PROXY + SSL_CERT_FILE, api.githubcopilot.com + per-plan subdomains whitelisted (#1049)
440
231
  bili amp # Amp CLI (Sourcegraph, closed Go binary) — cert-MITM via HTTPS_PROXY + SSL_CERT_FILE, ampcode.com whitelisted (#1049)
441
- bili goose # Goose (Block, Rust/reqwest): rustls release builds trust no CA file — no proxy envs at all; built-in openai/anthropic legs redirected via OPENAI_HOST/ANTHROPIC_HOST, custom declarative providers via a regenerated GOOSE_PATH_ROOT overlay with base_url /bili/ rewrites (real config untouched, user edits merged back); fixed third-party providers get a warning (#1049)
232
+ bili goose # Goose (Block, Rust/reqwest): rustls release builds trust no CA file — openai/anthropic legs via OPENAI_HOST/ANTHROPIC_HOST, custom providers via a regenerated GOOSE_PATH_ROOT overlay (/bili/ rewrites, real config untouched) (#1049)
442
233
  bili pi --mitm-domain api.foo.com # add a domain to the MITM whitelist
443
234
  ```
444
235
 
@@ -459,13 +250,7 @@ client baseURL before: https://api.openai.com/v1
459
250
  client baseURL after: http://localhost:8787/bili/https://api.openai.com/v1
460
251
  ```
461
252
 
462
- That's it — put your real API key in the client config as usual (the proxy
463
- passes it through untouched). Context windows (gpt-5.1-codex=400K,
464
- glm-5.2=1M, claude-opus-4=200K, …) are looked up from models.dev
465
- automatically.
466
-
467
- For per-client configuration examples (OpenCode, Codex, Pi, login-client
468
- MITM, …) see the web UI guide at [http://localhost:8787](http://localhost:8787).
253
+ For more per-client configuration examples, see the web UI guide at [http://localhost:8787](http://localhost:8787).
469
254
 
470
255
  **Verify.** With the proxy running and your config saved, check it answers
471
256
  and that your first real request shows compression activity in the log:
@@ -484,464 +269,60 @@ Then send one message from your client and watch the log
484
269
  should see a `processTurn` line per request, and once the conversation grows,
485
270
  `[acp-usage] round N input=X cached=Y (cache hit Z%)` + a `compress` event.
486
271
 
487
- ### dsh (deepseek-harness)
488
-
489
- Two lanes, same plugin (#941):
490
-
491
- - **Launcher:** `bili dsh` injects the full native plugin through a
492
- `--patch` overlay (`~/.dsh-bili/.bili-acp.patch.yml`) — every profile
493
- boots with the bili tools registered natively, model requests carry
494
- `x-bili-plugin` + the dsh session id (plugin mode), and `/acp` is
495
- session-bound. dsh's native auto-compaction is disabled in the same patch
496
- (`compaction-basic` → `auto: false`); manual `/compact` stays available.
497
- - **Profile install (no launcher) — one lane (#966):** `bili plugin install
498
- dsh` runs `dsh plugin --profile <name> add billion-context` for every
499
- existing profile — pnpm installs the package into each profile's own
500
- `node_modules`, and dsh mounts the bundled patch layer
501
- (`dsh.bundle.patch.yml`) automatically. The spec follows how bili itself
502
- was installed (#925): an npm-form install passes the registry name, a
503
- checkout/dev build passes its absolute path (a `link:` dependency, so
504
- local work stays live). Legacy managed blocks (`# bili begin` /
505
- `# bili end`, written by pre-#966 installs) are stripped on install and
506
- remove — user entries and comments survive, an emptied file gets its
507
- placeholder `[]` back. Run dsh once in each profile first so the profile
508
- dirs exist. The plugin spawns its own proxy at load (attaches to a healthy
509
- one instead of doubling; parent-pid watchdog), rewrites model-API traffic
510
- to `<proxy>/bili/<upstream-url>` via a global fetch patch, registers the
511
- manifest tools verbatim, and gates plugin-mode headers on tool readiness
512
- (round 1 rides wire mode). Opt-out: `BILI_NATIVE_DSH=0`. Remove with
513
- `bili plugin remove dsh` or `dsh plugin --profile <name> remove
514
- billion-context` — both go through the same channel. Registry installs
515
- require a published release that carries `dsh.bundle.patch.yml`. If dsh
516
- fails to boot right after an add with `ERR_MODULE_NOT_FOUND` on
517
- `billion-context/dsh`, the profile resolved a pre-bundle copy from a stale
518
- package-metadata cache (#953) — re-add pinned: `dsh plugin --profile
519
- <name> add billion-context@latest`.
520
- - **Auto-update keeps profiles in lockstep:** the refresh has two triggers —
521
- after a global self-update, AND from the **profile copy's own proxy** when
522
- its periodic check sees a newer registry version (so dsh plugin-market
523
- users with no global bili running still refresh, #1196). Both scan
524
- `~/.dsh/profiles/*/package.json` and bring any registry-pinned
525
- `billion-context` dependency to the target version (the new global version
526
- for the global trigger, registry-latest for the self trigger), always
527
- through dsh's own `plugin add` channel — never an in-place copy — so the
528
- loaded plugin and the proxy never drift apart again (#953); profiles
529
- pinned to a local source are left alone. The refresh is best-effort,
530
- retries next cycle on failure, and never fails the update or the proxy.
531
- - **Reported: zero proxy traffic for some transports under profile install
532
- (#1158, under investigation):** sessions served by some of dsh's
533
- `llm-pi-ai`-layer transports show NO model request ever reaching the proxy
534
- (no `processTurn` logged; bili tools 404 with "no model request has
535
- arrived") while other providers in the same host work normally. The root
536
- cause is still being pinned down with runtime evidence — candidates: the
537
- transport-level fetch shape (SDK-injected fetch / non-global dispatcher) or
538
- a host-side attribution gap leaving the traffic unclaimed by the takeover
539
- gate. Detection: the proxy logs a one-time `[plugin] NO MODEL REQUESTS seen
540
- for conversation …` warning, and the dsh plugin logs each distinct endpoint
541
- the attribution gate lets through unproxied (once per process). Reliable
542
- workaround meanwhile: launch through `bili dsh` instead — the launcher's
543
- settings overlay rewrites those providers' `baseURL`s to `/bili/` URLs, so
544
- the traffic reaches the proxy regardless of which fetch the transport uses
545
- or what the attribution state is.
546
-
547
- Under a `bili dsh` launch the plugin ATTACHES to the launcher's proxy (no
548
- second spawn). Raw upstream URLs rewrite to `<proxy>/bili/<url>` like
549
- spawn mode (a loopback proxy target is never proxied, so the MITM envs are
550
- simply bypassed); already-routed `/bili/`-prefixed requests pass through
551
- untouched except for header stamping. Known limitation: manual
552
- `/compact` has no dsh-side event hook, so its boundary is left to the
553
- kernel's natural ingest diff (auto-compaction is off, so this is rare).
554
-
555
- ### Kimi Code (Moonshot)
556
-
557
- Three aligned modes: `bili kimi` (launcher, cert-MITM — Option 2), `/bili/`
558
- URL prefix, and native plugin mode (`bili plugin install kimi`, #963). Kimi
559
- Code v2's plugin system is declarative only (`kimi.plugin.json`: MCP servers,
560
- hooks, skills — no in-process JS execution), so bili cannot patch the client's
561
- fetch stack like it does for pi/opencode/dsh. Instead the plugin ships two
562
- small node scripts that do the work around the client:
563
-
564
- - **Install:** `bili plugin install kimi` writes
565
- `$KIMI_CODE_HOME/plugins/managed/billion-context/kimi.plugin.json`
566
- declaring a stdio MCP server (`node <root>/dist/kimi/native-mcp.js`) plus a
567
- `SessionStart` hook (`node <root>/dist/kimi/bootstrap-hook.js`, 30 s
568
- timeout), and registers the plugin in
569
- `$KIMI_CODE_HOME/plugins/installed.json`. The installer requires
570
- `kimi --version` ≥ 2.0.0 and refuses below that (the launcher still works
571
- either way). Remove with `bili plugin remove kimi` (managed dir + registry
572
- record + config restore).
573
- - **Per-session bootstrap:** kimi spawns the MCP server as a direct child for
574
- each session; at startup it attaches to a healthy proxy
575
- (`BILLION_CONTEXT_PROXY`) or spawns its own on an ephemeral port, then
576
- rewrites the client's routing with an idempotent, line-surgical managed
577
- block in `~/.kimi-code/config.toml`: an own provider `[providers.bili]`
578
- (`base_url = http://127.0.0.1:<port>/bili/<upstream>`, cloning the active
579
- provider's `oauth` / `api_key` reference verbatim), a `[models.bili-kimi]`
580
- alias, and a top-level `default_model` redirect with the previous value
581
- recorded inside the block. The original file is snapshotted to
582
- `config.toml.bili-bak` once; every write happens under a mkdir lockfile and
583
- user content outside the block is never touched. Kimi's config hot-reload
584
- applies the change to live sessions. The `SessionStart` hook runs the same
585
- bootstrap opportunistically (attach-only — it never spawns); its
586
- non-blocking race is tolerated by design: round 1 may ride direct/wire mode,
587
- and the invariant is never pointing `base_url` at a dead port.
588
- - **Plugin-mode stamping:** the block gains
589
- `custom_headers = { x-bili-plugin = "kimi" }` ONLY after the ACP tool list
590
- has been verified against the live proxy manifest — until then traffic rides
591
- wire mode. Because `custom_headers` are static per provider they cannot
592
- carry per-request window/model headers without going stale on model switch;
593
- the runtime-info report therefore happens at bootstrap only (model + context
594
- window + max output from the client's own config whenever present).
595
- - **Watchdog & lifecycle:** the MCP child probes the proxy every 30 s. In
596
- attach mode it waits forever (it never touches a user-owned proxy); in spawn
597
- mode a dead proxy is respawned and the routing rewritten to the new origin.
598
- If recovery fails, the managed block is removed so traffic degrades back to
599
- direct upstream rather than hitting a dead port. When a session ends, kimi
600
- kills the MCP child and the parent-pid watchdog tears down the spawned
601
- proxy. Multiple concurrent TUIs share the first-spawned proxy; when it goes
602
- away the remaining sessions respawn and re-route automatically.
603
- - **Known limitations:** subagent conversations get their own derived proxy
604
- sessions (kimi exposes no stable session id; tool calls bind via the
605
- per-call `conversation_id` argument), and kimi's native auto-compaction is
606
- NOT pushed out — ACP compression simply fires first, as in launcher mode.
607
- Opt-out: `BILI_NATIVE_KIMI=0`.
608
-
609
- ### Hermes (Nous Research)
610
-
611
- Three aligned modes: `bili hermes` (launcher, cert-MITM — Option 2), `/bili/`
612
- URL prefix, and native plugin mode (`bili plugin install hermes`, #958). The
613
- hermes CLI agent's plugin API is Python-only (the `desktop/plugin.js` SDK
614
- belongs to the separate Desktop app), so the native plugin is a small
615
- pure-stdlib Python module shipped inside the npm package:
616
-
617
- - **Install:** `bili plugin install hermes` copies `plugin.yaml` +
618
- `__init__.py` into `~/.hermes/plugins/billion-context/`, writes a
619
- machine-owned `bili.json` sidecar pointing at the global bili install
620
- (`dist/index.js` + node path), and enables the plugin through hermes' own
621
- channel (`hermes plugins enable billion-context` — if the CLI isn't on PATH
622
- the same command is printed instead). Start a new hermes session to
623
- activate. Remove with `bili plugin remove hermes`; refresh with
624
- `bili plugin update hermes` after a global update.
625
- - **Lifecycle:** at load the plugin attaches to a healthy running proxy or
626
- spawns its own on an ephemeral port (parent-pid watchdog tears it down when
627
- hermes exits; concurrent starts arbitrate through the same starting-marker
628
- protocol the launcher uses). Only once the proxy is verified healthy does it
629
- point hermes' httpx stack at it via `HTTPS_PROXY` / `https_proxy` +
630
- `SSL_CERT_FILE` (bili's combined CA bundle — current hermes resolves ambient
631
- trust there; `HERMES_CA_BUNDLE` stays set for older builds) —
632
- `~/.hermes/config.yaml` is never touched. Provider https hosts are read from hermes' config and whitelisted
633
- for MITM; everything else blind-tunnels exactly like launcher mode. If no
634
- proxy can be made healthy, the plugin stands down silently and traffic goes
635
- direct (no compression, no dead port).
636
- - **Plugin-mode stamping:** an `llm_request` middleware stamps
637
- `x-bili-plugin: hermes` + conversation id (= the hermes session id, so
638
- gateway multi-session stays safe) + model, and `x-bili-plugin-max-output`
639
- once known — ONLY after the ACP tools are registered against the live proxy
640
- manifest; round 1 rides wire mode. A `pre_api_request` hook captures the
641
- effective `max_tokens` and pushes runtime-info (model + max output) to the
642
- proxy. `compress` / `decompress` / `acp_status` are registered as real
643
- hermes tools served by the proxy's existing plugin endpoints.
644
- - **Known limitations:** requests going out hermes' Codex-wire transport may
645
- drop the per-request header surface, so such setups stay in wire mode until
646
- that transport exposes headers. Inert when `BILLION_CONTEXT_PROXY` is set
647
- (the launcher owns the proxy) or `BILI_PROVIDER_REWRITES` is defined.
648
- Opt-out: `BILI_NATIVE_HERMES=0`.
649
-
650
- ### ZCode (Z.ai / bigmodel coding plan)
651
-
652
- Three aligned modes: `/bili/` URL prefix, cert-MITM through the GUI's
653
- Settings → Network (HTTP proxy + root-CA path), and native plugin mode
654
- (`bili plugin install zcode`, #1145). ZCode's extension surface is
655
- Claude-Code-shaped but declarative: user-level hooks and stdio MCP servers in
656
- `~/.zcode/cli/config.json`, no in-process JS seam. So the native lane ships
657
- two small node scripts that do the work around the client:
658
-
659
- - **Install:** `bili plugin install zcode` writes `~/.zcode/cli/config.json`:
660
- sets `hooks.enabled = true`, appends a `SessionStart` process hook
661
- (`node <root>/dist/zcode/bootstrap-hook.js`) and registers a stdio MCP
662
- server `mcp.servers.bili` (`node <root>/dist/zcode/mcp-entry.js`). A
663
- pre-existing user-owned `mcp.servers.bili` entry is never overwritten — the
664
- installer refuses loudly instead. No URL is frozen at install time; routing
665
- happens per session. Remove with `bili plugin remove zcode` (strips only
666
- bili's entries, reverts `hooks.enabled` when it was the one to enable it,
667
- and restores the provider store from its snapshot).
668
- - **Per-session bootstrap:** each ZCode session spawns the MCP child as a
669
- direct process; at startup it attaches to a healthy proxy
670
- (`BILLION_CONTEXT_PROXY`) or spawns its own on an ephemeral port, then
671
- rewrites the active provider store with idempotent JSON surgery under a
672
- mkdir lockfile: the bigmodel coding-plan provider entries' `baseURL` becomes
673
- `http://127.0.0.1:<port>/bili/<upstream>` (the builtin default upstream is
674
- `https://open.bigmodel.cn/api/anthropic`; any custom baseURL you set is
675
- preserved verbatim behind the wrapper). Both store generations are handled:
676
- legacy `~/.zcode/v2/config.json` (`provider.<id>.options.baseURL`) and the
677
- v3.14+ personal store `~/.zcode/v2/provider_config.json`
678
- (`config.providerConfigRules.providerRules[].config.api.baseUrl`) — when
679
- both exist, the new store wins. The original file is snapshotted to
680
- `<file>.bili-bak` once per user edit (the snapshot always reflects your last
681
- real state, never bili's own writes); every other key is preserved
682
- byte-for-byte. Legacy-generation clients load provider config at startup —
683
- restart ZCode once after installing; newer builds pick up routing changes
684
- mid-session (~1 s polling). The `SessionStart` hook runs the same bootstrap
685
- opportunistically (attach-only — it never spawns); its non-blocking race is
686
- tolerated by design: round 1 may ride wire mode, and the invariant is never
687
- pointing `baseURL` at a dead port.
688
- - **Plugin-mode stamping:** once the MCP child verifies the ACP tool list
689
- against the live proxy manifest, the routed entries gain
690
- `headers["x-bili-plugin"] = "zcode"` — until then traffic rides wire mode.
691
- Tool calls bind via the per-call `conversation_id` argument (#760).
692
- - **Watchdog & lifecycle:** the MCP child probes the proxy every 30 s. In
693
- attach mode it waits forever (it never touches a user-owned proxy); in spawn
694
- mode a dead proxy is respawned and the routing rewritten to the new origin.
695
- If recovery fails, the managed rewrite is removed so traffic degrades back
696
- to direct upstream rather than hitting a dead port. When a session ends,
697
- ZCode kills the MCP child and the parent-pid watchdog tears down the spawned
698
- proxy. Concurrent sessions share the first-spawned proxy; when it goes away
699
- the remaining sessions respawn and re-route automatically.
700
- - **Known limitations:** ZCode's anti-fraud fingerprinting (#661) applies to
701
- MITM-rebuilt bodies on `zcode.z.ai` login traffic — native mode does not
702
- touch that surface (model traffic flows through the provider store, not the
703
- GUI proxy); if you also run the GUI-proxy/MITM setup, keep the
704
- `"mitm://zcode.z.ai": { "passthrough": true }` route. Inert when
705
- `BILLION_CONTEXT_PROXY` is set (attach mode owns the proxy) or
706
- `BILI_PROVIDER_REWRITES` is defined. Opt-out: `BILI_NATIVE_ZCODE=0`.
707
-
708
- ### Gemini family (Gemini CLI / iFlow CLI / Qwen Code)
709
-
710
- Three launchers for the gemini-cli architecture family (#1043 tier 1). Two of
711
- the three have a base-URL env hook; one doesn't:
712
-
713
- - **`bili gemini`** — Gemini CLI (`@google/gemini-cli`). Sets
714
- `GOOGLE_GEMINI_BASE_URL=<proxy>/bili/<upstream>` (default upstream
715
- `https://generativelanguage.googleapis.com`; if you export your own
716
- `GOOGLE_GEMINI_BASE_URL`, that value is relayed through the proxy instead).
717
- The client switches to its `gateway` auth mode and sends Google-native-wire
718
- requests straight to the loopback proxy — no MITM, no CA install, and
719
- `~/.gemini` is never touched. The proxy speaks this wire natively (model
720
- name rides in the URL path). Limitations: headless `-p` runs need a saved
721
- auth selection (e.g. `security.auth.selectedType = "gemini-api-key"` in
722
- settings + `GEMINI_API_KEY`) because gemini-cli rejects purely-env-derived
723
- gateway auth in non-interactive mode; users on an OAuth personal login
724
- (CodeAssist) are not covered by this route at all — that path ignores the
725
- base-URL hook.
726
- - **`bili iflow`** — iFlow CLI (`@iflow-ai/iflow-cli`). Same pattern via
727
- `IFLOW_BASE_URL` (default `https://apis.iflow.cn/v1`, relayed when you set
728
- it); OpenAI chat-completions wire.
729
- - **`bili qwen`** — Qwen Code (`QwenLM/qwen-code`). This fork dropped the
730
- base-URL hook (`DASHSCOPE_PROXY_BASE_URL` is a header-tuning knob, not
731
- routing), but it honors standard proxy envs, so the launcher uses cert-MITM:
732
- `HTTPS_PROXY=<proxy>` + `NODE_EXTRA_CA_CERTS=<bili CA>` with a static
733
- whitelist of the default model hosts (DashScope / Qwen gateway / common
734
- third-party endpoints). Custom relay hosts: add them with
735
- `--mitm-domain <host>`. Best-effort route — a `BLIND TUNNEL WARNING` in the
736
- log means a host is missing from the whitelist.
737
-
738
- None of the three has a native mode: none exposes an in-loop tool injection
739
- seam (gemini-cli extensions reach custom commands only; the forks inherit
740
- that surface). Launcher-only by design.
741
-
742
- ### Client uses `http.proxy` (CONNECT) but nothing compresses
743
-
744
- Some clients (VS Code-based IDEs: CodeBuddy, Cursor, Windsurf, …) only offer an HTTP **proxy** setting (`http.proxy`, `codingcopilot.httpProxyURL`, …) — no model base-URL to rewrite. Such clients send `CONNECT <model-host>:443` through the proxy instead of plain `/bili/…` requests. That path is only decrypted when the model host is on bili's **MITM whitelist**; otherwise bili blind-tunnels the TLS bytes (opaque relay) and can never see — or compress — the model requests (#897).
745
-
746
- This failure mode is now loud instead of silent:
747
-
748
- - a one-time `BLIND TUNNEL WARNING` per target host in the log, with the fix steps;
749
- - `blindTunnels` (count + exact target hosts) in `curl -s http://localhost:8787/__bili/health` and `/__bili/stats` (loopback-only);
750
- - an `UNDECRYPTED TRAFFIC (instance-level)` section in `acp_status` output while such tunnels exist.
751
-
752
- To actually compress such a client: add its model domain to `"mitm".domains` in `billion-context.json` (e.g. `"mitm": { "domains": ["copilot.tencent.com"] }`) or via `BILI_MITM_DOMAINS`, restart bili, and make the client trust bili's root CA (`NODE_EXTRA_CA_CERTS=~/.local/share/billion-context/ca/root-ca.pem` for Node-based clients, or the client's own CA-path setting). The `/bili/` prefix trick does not apply here — there is no URL to change. Details: [CONFIGURATION.md → MITM](CONFIGURATION.md#mitm-transparent-proxy-login-clients).
753
-
754
- ### An unrecognized endpoint goes direct and nothing compresses (#1290)
755
-
756
- bili only compresses requests whose path matches a known wire protocol (`/chat/completions`, `/llm_raw_chat`, `/v1/messages`, `/responses`, …). A request to any other path — e.g. a third-party plugin's **custom wire** such as Command Code's Go plan posting to `/alpha/generate` — is relayed byte-for-byte and **never compressed**. There is no config seam to declare an arbitrary new wire today; adding one is a separate feature, not a switch you can flip.
757
-
758
- That outcome is now loud instead of silent (#1290):
759
-
760
- - the client-side fetch hook logs each distinct unrouted endpoint once per process (`…is not a recognized model endpoint, so bili did not route it through the proxy…`);
761
- - `unrecognizedPaths` (per-path counts) in `curl -s http://localhost:8787/__bili/stats` (loopback-only);
762
- - an `UNRECOGNIZED PATHS (instance-level)` section in `acp_status` output while such requests exist.
763
-
764
- If you expected compression at such an endpoint, use the provider's standard protocol endpoint instead (Command Code's Provider plan posts to `/provider/v1/chat/completions`, which bili does compress); a genuinely custom wire needs its own support.
765
-
766
- ## OpenCode
767
-
768
- One bundled plugin serves **both** OpenCode generations: the agent file keeps
769
- the V1 `server()` export alongside the V2 `setup()`, so hosts ≥ 1.18.29 load
770
- the V1 shape and 2.x hosts load the V2 `setup()`. The standalone
771
- [`opencode-acp`](https://github.com/ranxianglei/opencode-acp) extension is
772
- V1-only and does **not** load under 2.x — for OpenCode 2.x, billion-context
773
- is the recommended context manager. Everything below is verified end-to-end
774
- on `@opencode/cli` 2.0.3 (V1 lane: 1.14.46 and 1.18.31).
775
-
776
- | Path | Command | When |
777
- |---|---|---|
778
- | Launcher (easiest) | `bili opencode` | one command brings up proxy + client; real config untouched |
779
- | Native (no launcher) | `bili plugin install opencode` | self-spawning plugin in your real config; start `opencode` as usual |
780
- | Pure proxy (fallback) | baseURL `/bili/` prefix | no plugin — wire-level tool injection |
781
-
782
- ### Launcher — `bili opencode`
783
-
784
- HTTPS rides cert-MITM, HTTP a temp `opencode.json` clone with `/bili/`
785
- (JSONC comments accepted, merged the way opencode itself merges them;
786
- relative local plugin specs re-anchored to absolute paths in the clone —
787
- opencode resolves them against the declaring config file's dir, #826). Host
788
- generation is detected with a `--version` probe (failed probe defaults to
789
- 1.x): on a **2.x** host the built-in V2 plugin (`dist/agent/opencode.js`) is
790
- injected as a temp wrapper directory whose `index.js` re-exports the plugin
791
- file (2.x rejects bare file paths in the config `plugin` array); **1.x**
792
- hosts get the bare file path.
793
-
794
- What the plugin does (both generations): registers the bili tools natively
795
- in-host — compress / decompress / search_context / acp_status (+ absorb) —
796
- and stamps the proxy headers on every outgoing provider request, including
797
- context-window / max-output read from the host's own model catalog
798
- (`ctx.catalog.model.list()`, refreshed every 60s) and reported to the proxy
799
- as runtime-info (#955) — compression runs in plugin mode with **no**
800
- wire-level tool injection. Native auto-compaction is disabled automatically
801
- (`compaction.auto: false`). Every registration is defensive (optional
802
- chaining): on any 2.x build where a seam is missing or never fires, the
803
- plugin stays inert and the session transparently runs in plain proxy mode
804
- instead of breaking — observed across adjacent `dev` builds whose API
805
- surfaces differ from each other (#754 review probes).
806
-
807
- 1.x specifics (verified 1.14.46 + 1.18.31): the V1 `.server()` hooks rewrite
808
- every provider `options.baseURL` to `<proxy>/bili/…` in-process and set
809
- `compaction.auto: false`; `chat.headers` stamps the plugin headers per
810
- request; `tool` registers the bili tools with real zod shapes (zod is a
811
- runtime dependency — when it cannot be resolved the plugin degrades to
812
- rewrite-only). Providers **without** an explicit `baseURL` (SDK defaults,
813
- e.g. bare `@ai-sdk/openai` → api.openai.com) are caught by a global `fetch`
814
- patch (log: `v1: fetch patch installed`) — idempotent, passes
815
- `/bili/`-wrapped URLs through untouched; verified including the OpenAI
816
- Responses endpoint.
817
-
818
- ### Native (no launcher) — `bili plugin install opencode`
819
-
820
- Registers a self-spawning plugin in your real opencode config and sets
821
- `compaction.auto: false`; afterwards plain `opencode` works as-is. No MCP
822
- face is added by default (the native plugin already provides the bili tools,
823
- session-bound); pass `--with-mcp` to add one — the entry then carries no
824
- origin pin, so it survives the plugin's ephemeral-port proxy restarts (#926).
825
- Entry form depends on how THIS bili was installed: an **npm install** writes
826
- the bare package name (`"plugin": ["billion-context"]`) — the package
827
- publishes `exports["./server"]` → `dist/agent/opencode-native.js`, so
828
- opencode loads it through its own Npm.add machinery; zero absolute paths, portable. (That exact bare-name entry doubles as a hand-install without bili — see Option 1.) A **git checkout / dev build** falls back to a local shim dir
829
- (`<configDir>/plugins/billion-context/index.js` → this checkout's
830
- `dist/agent/opencode-native.js`) — machine-local by construction; re-running
831
- install from an npm install migrates the entry back to the bare name.
832
-
833
- At load the plugin bootstraps its own proxy (attaches to a healthy instance
834
- instead of doubling; parent-pid watchdog kills it when opencode exits),
835
- routes model-API traffic to `<proxy>/bili/<upstream-url>`, and exposes the
836
- same native bili tools as launcher mode — no fixed port, no env var, no
837
- launcher. Opt-out: `BILI_NATIVE_OPENCODE=0`. If no proxy can be made
838
- healthy, requests go direct (uncompressed) with a one-time warning and
839
- recover automatically. Under a `bili opencode` launch this entry is skipped
840
- entirely (the launcher owns the proxy).
841
-
842
- ### Pure proxy (no plugin)
843
-
844
- Point the provider baseURL at the proxy like any other client:
845
-
846
- ```json
847
- {
848
- "provider": {
849
- "myprovider": {
850
- "npm": "@ai-sdk/openai-compatible",
851
- "options": {
852
- "baseURL": "http://localhost:8787/bili/http://upstream.example/v1",
853
- "apiKey": "sk-any"
854
- }
855
- }
856
- }
857
- }
858
- ```
859
-
860
- Note: 2.0 AI-SDK providers require an `apiKey` field even for local
861
- endpoints that never check it — set any non-empty value.
862
-
863
- ### Status: `/acp` and `acp_status`
864
-
865
- The `/acp` panel is session-bound in all modes, and the `acp_status` tool is
866
- its in-host equivalent everywhere. On 2.0.x stable, where the command editor
867
- supports adding entries (`editor.add`), the V2 plugin additionally registers
868
- an `/acp` slash command — rendered as a synthetic non-model message,
869
- panel-first like the `acp_status` tool; on older shapes the registration
870
- stays inert. Note `opencode run` mode dispatches no slash commands at all
871
- (they pass through to the model) — use the TUI.
872
-
873
- The same seam carries `/acp-cache` (#1146) — the human entry point to the
874
- prompt-cache reconciliation report (identical output to the `acp_cache` tool):
875
- pi/omp register it natively (`/acp-cache [full]` for the every-line listing);
876
- opencode V1 renders it as an ignored message the proxy strips from model
877
- context before it reaches the wire; opencode V2 as a synthetic message (report
878
- visible up to ~8 KB); dsh (both lanes) shows the default summary ledger — dsh's
879
- command API passes no arguments, so there is no `full`. Legacy opencode-acp
880
- sessions (#920) get an explicit unavailable notice instead (their traffic
881
- bypasses this proxy's compression state). Claude Code has no in-process command
882
- API: `bili plugin install claude` writes a model-mediated
883
- `commands/acp-cache.md` markdown command whose prompt drives the `acp_cache`
884
- MCP tool and pastes the report back verbatim. codex/kimi/hermes expose no
885
- user-typable command seam — ask the model to call its `acp_cache` tool directly.
886
- Per-fold P&L verdicts are priced by the optional `compress.priceProfile`
887
- (normalized multipliers over the input-token unit); when no level sets it, the
888
- request model's models.dev price row applies in absolute $/Mtok (kernel ratio
889
- defaults only for unresolvable models) — breakeven/PAID BACK therefore reflect
890
- your upstream's actual economics out of the box; override per provider for
891
- relays with custom markup (CONFIGURATION.md, #1279).
892
-
893
- The same seam carries `/acp-rule` (#1251/#1399) — the human entry point to
894
- the persistent-rules feature (identical output to the `acp_rule` tool):
895
- pi/omp register it natively with the tool's full operation set — bare
896
- `/acp-rule` lists every recorded rule, `/acp-rule <text>` records one directly
897
- (as if the model had called it), `/acp-rule remove <id>` deletes one, and bare
898
- `/acp-rule clear` wipes all recorded rules (`clear <text>` records instead of
899
- wiping — a typo must not destroy every rule). The wrapped transcript message
900
- is stripped from model context by content signature like the cache report —
901
- recorded rules reach the model every turn via the system-prompt injection
902
- anyway.
903
-
904
- ### Legacy opencode-acp sessions (#920)
905
-
906
- On 1.x hosts, pre-migration [`opencode-acp`](https://github.com/ranxianglei/opencode-acp)
907
- sessions keep working under both lanes: the launcher strips the
908
- `opencode-acp` entry from its temp config clone (the host never loads it
909
- armed), and each lane absorbs the installed package (imported directly from
910
- `node_modules` — `.opencode/node_modules`, project `node_modules`, global npm
911
- root, or opencode's config-scope modules, first hit wins). A session is
912
- legacy iff opencode-acp's persisted state file exists
913
- (`<XDG_DATA_HOME>/opencode/storage/plugin/acp/<sessionID>.json`, or the dir
914
- from `storagePath` in `acp.jsonc`):
915
-
916
- - **Legacy session** — compression runs through the absorbed opencode-acp
917
- (its own refs and block store keep working: `compress` / `decompress` /
918
- `search_context` / `acp_status` / `acp_context_recap` all execute in it).
919
- Its model requests carry `x-bili-plugin-bypass: 1`; the proxy forwards
920
- them VERBATIM — no wire injection, no nudge, no session binding.
921
- - **New session** — bili owns it: tool calls forward to the proxy's plugin
922
- endpoints (plugin mode). The executor routes by session lane, so a new
923
- session's `compress` reaches the proxy while a legacy session's reaches
924
- opencode-acp. `acp_context_recap` has no proxy counterpart — on new
925
- sessions the proxy answers with its unknown-tool message.
926
-
927
- `/acp` and `/dcp` route the same way. Adoption of new sessions into
928
- opencode-acp's registry is prevented by gating its transforms (system /
929
- messages / text.complete) on the legacy predicate. Degradation: when the
930
- package is absent or fails to import (or isn't v1), bili runs alone and
931
- legacy sessions behave as read-only archives (old tags render, `decompress`
932
- returns `[Block … not found]`, new refs restart from m00001).
933
-
934
- ### Caveats
935
-
936
- - The 2.x line publishes as npm package `@opencode/cli`, and its plugin API
937
- surface is still moving between builds (adjacent `dev`-channel builds
938
- expose different `ctx` shapes) — the hook/tool details above are
939
- version-specific observations, not a stable contract.
940
- - Design note: the V2 plugin is a thin protocol client (no acp-kernel
941
- inside) because the proxy stays the single compression authority — that
942
- eliminates kernel-version drift between agent and proxy; it does not rely
943
- on the plugin API being unable to mutate context (that capability varies
944
- by 2.x build).
272
+ ### Client deep dives
273
+
274
+ Everything that doesn't fit in one Quickstart line — how each client's lanes attach, what gets written where, and known limitations — lives in **[CLIENTS.md](CLIENTS.md)**: dsh · Kimi Code · Hermes · ZCode · Gemini family (Gemini CLI / iFlow CLI / Qwen Code) · cert-MITM clients that never compress (CONNECT blind tunnels, #897) · unrecognized endpoints going direct (#1290) · OpenCode (launcher / native / pure proxy, `/acp` status & rules, legacy opencode-acp sessions #920).
275
+
276
+ ## FAQ
277
+
278
+ **How do I check my cache hit rate?** Don't dig through logs — `/acp-cache`
279
+ prints a **text summary report right in the client**, headed by a clickable
280
+ **Web UI link**: open it for the web version of the session page — the cache
281
+ hit-rate **line chart** plus per-break **attribution**:
282
+
283
+ ![web session page: cache hit-rate chart + attribution](docs/cache-web-session.en.png)
284
+ The report has four
285
+ blocks that pin things down at a glance: **GRAND LEDGER** (totals + hit% with
286
+ an explicit `HEALTHY` verdict; misses decomposed into new content /
287
+ compress re-pay / upstream-ttl-or-client-rewrite) · **FOLD ECONOMICS** (per-fold
288
+ economics: net tokens saved, paid-back verdicts) · **LINE ITEMS** (anomalies
289
+ only: hit<85% or miss≥5000). Rule of thumb: **compression itself costs ≤2%** —
290
+ a healthy session sits at **95–97%**. When you see less, the attribution tells
291
+ you which of the usual suspects it was, in this order: ① upstream cache TTL
292
+ expiry (shows up as stable-prefix misses — the top-spikes line names idle
293
+ times), ② a model switch, ③ a bili bug (report it with the page attached),
294
+ ④ other/unknown. `/acp-cache [full]` lists every fold & line; same report over
295
+ HTTP: `GET /__bili/cache-report`; the raw per-request `[acp-usage]` lines still
296
+ land in the log file for deep dives.
297
+
298
+ **What does `/acp` show?** In clients with the native plugin (opencode, dsh),
299
+ `/acp` renders the ACP status panel of the current conversation straight from
300
+ the proxy (session, blocks, compressible ranges, usage); before the first model
301
+ request it shows an idle notice instead. `/acp-cache [full]` prints the cache
302
+ report above.
303
+
304
+ **Can I query sessions and config over the web?** Yes — open
305
+ [http://localhost:8787](http://localhost:8787): an overview dashboard, session
306
+ list with per-session detail, live logs, a config editor, and an upstream
307
+ connectivity test. Everything is also plain JSON for scripting
308
+ (`/__bili/stats`, `/__bili/sessions`, `/__bili/config`, …).
309
+
310
+ **When does compression happen?** It is model-driven: the injected context
311
+ tools are called by the model as context grows, gentle growth nudges
312
+ (~50K-token steps by design, adjustable via `compress.nudgeGrowthTokens`)
313
+ prompt it along the way, and preflight fires as a hard backstop when the input
314
+ alone exceeds the window (#470). Watch it live with `/acp` or the web UI.
315
+
316
+ **Is bili transparent? How do I turn it off?** Unrecognized endpoints forward
317
+ unchanged ([CLIENTS.md](CLIENTS.md)), and every mode reverses cleanly:
318
+ `bili plugin remove <client>` for native installs, stop using the launcher
319
+ command / env vars / `/bili/` prefix for the other two — traffic goes direct
320
+ again immediately.
321
+
322
+ **Where are logs and session data stored?** Log:
323
+ `~/.local/state/billion-context/bili.log` (also mirrored to stderr); session
324
+ state: `~/.local/share/billion-context/` (XDG-overridable; Windows AV exclusion
325
+ #362, opt-in cleanup #1082) — full paths in [CONFIGURATION.md](CONFIGURATION.md).
945
326
 
946
327
  ## Running the proxy
947
328
 
@@ -962,94 +343,25 @@ Flags override env vars and the config file. `bili --help` lists them all.
962
343
 
963
344
  ### Remote agents (`--host`)
964
345
 
965
- By default the proxy binds `127.0.0.1` and only accepts loopback
966
- connections. To serve agents on other machines, bind a non-loopback host:
346
+ By default the proxy binds `127.0.0.1` and only accepts loopback connections. To serve agents on other machines: `bili --host 0.0.0.0` (or your LAN IP); remote agents point their model `baseURL` at `http://<this-host>:<port>/bili/…`.
967
347
 
968
- ```bash
969
- bili --host 0.0.0.0 # all interfaces (or use your LAN IP)
970
- ```
971
-
972
- - Remote agents point their model `baseURL` at `http://<this-host>:<port>/bili/…`.
973
- - MITM-mode `CONNECT` then also accepts remote clients — for **whitelisted
974
- model hosts only**. Blind tunnels to arbitrary hosts stay loopback-only, so
975
- the proxy can never be used as an open relay.
976
- - The `/bili/<absolute-url>` tunnel has destination admission (#409): the
977
- proxy itself and link-local/metadata addresses are **always denied**;
978
- loopback/private destinations are allowed for local clients (self-hosted
979
- upstreams) and **denied for remote clients** unless listed in
980
- `BILI_TUNNEL_ALLOWED_HOSTS` (`host` or `host:port`, comma-separated) — a
981
- remote peer must not use the proxy as an SSRF pivot into your LAN, and the
982
- management plane is unreachable through the tunnel even via NAT hairpin
983
- (tunneled requests carry an internal `x-bili-tunnel` marker that `/__bili/`
984
- rejects). One exception (#1073): a **local** client relaying a management
985
- path (`/__bili/*`, `/__acp/*`) to a **loopback IP-literal** destination is
986
- forwarded unmarked — any same-machine process can already reach that port
987
- directly, so the marker would add no protection while breaking legitimate
988
- inter-instance health probes. Remote peers and hostname destinations keep
989
- the marker unconditionally. An absolute-form request addressed to the
990
- instance's **own** endpoint on a management path is served locally instead
991
- of tunneled (a forward-proxy-style health probe gets a real answer).
992
- - There is **no authentication**: only do this on a trusted LAN or behind a
993
- firewall. The `/__bili/` management endpoints remain loopback-only.
994
- - A startup `[security]` warning reminds you of the above.
348
+ - MITM-mode `CONNECT` then also accepts remote clients — for **whitelisted model hosts only**. Blind tunnels to arbitrary hosts stay loopback-only, so the proxy can never be used as an open relay.
349
+ - `/bili/<absolute-url>` destination admission (#409): the proxy itself and link-local/metadata addresses are always denied; loopback/private destinations are allowed for local clients (self-hosted upstreams) and denied for remote clients unless listed in `BILI_TUNNEL_ALLOWED_HOSTS` (`host` or `host:port`, comma-separated). One exception (#1073): a **local** client relaying a management path (`/__bili/*`, `/__acp/*`) to a **loopback IP-literal** destination is forwarded unmarked; remote peers and hostname destinations keep the internal tunnel marker unconditionally, and management paths stay unreachable through the tunnel even via NAT hairpin. An absolute-form request addressed to the instance's own endpoint on a management path is served locally instead of tunneled (a forward-proxy-style health probe gets a real answer).
350
+ - There is **no authentication**: only do this on a trusted LAN or behind a firewall. The `/__bili/` management endpoints remain loopback-only. A startup `[security]` warning reminds you of the above.
995
351
 
996
352
  ### Debugging
997
353
 
998
- Three ways to enable verbose logging (priority: flag > env > config):
999
-
1000
- 1. **CLI flag** (quickest): `bili --debug`
1001
- 2. **Env var**: `ACP_DEBUG=1 bili`
1002
- 3. **Config file**: `"debug": true` in `billion-context.json`
1003
-
1004
- Verbose mode logs every `processTurn` (tag counts, token usage), the nudge
1005
- decision (growth/usage/pendingT1/shouldInject), client headers, and SSE
1006
- rewrites.
354
+ `bili --debug` (or env `ACP_DEBUG=1`, or `"debug": true` in config — flag > env > config) logs every `processTurn` (tag counts, token usage), the nudge decision (growth/usage/pendingT1/shouldInject), client headers, and SSE rewrites.
1007
355
 
1008
356
  ### Log file
1009
357
 
1010
- All logs are **tee'd to a file by default**: `~/.local/state/billion-context/bili.log`
1011
- (XDG state dir). They also still print to stderr so a foreground `bili start`
1012
- shows them in the terminal.
1013
-
1014
- ```bash
1015
- # Config: "logFile": "/custom/path.log"
1016
- # Env: ACP_LOG_FILE=/custom/path.log (or ACP_LOG_FILE=off to disable the file)
1017
- ```
1018
-
1019
- The file auto-rotates at 10 MB (renamed to `bili.log.old`). Cache-hit stats
1020
- per request are logged as `[acp-usage] round N input=X cached=Y (cache hit Z%)`
1021
- so you can measure prefix-cache health directly from the log.
358
+ All logs tee to `~/.local/state/billion-context/bili.log` by default (XDG state dir) and still print to stderr. Override with `"logFile"` in config or `ACP_LOG_FILE` (`off` disables the file). Auto-rotates at 10 MB (`bili.log.old`). Per-request cache-hit stats log as `[acp-usage] round N input=X cached=Y (cache hit Z%)` so you can measure prefix-cache health directly from the log.
1022
359
 
1023
360
  ### Self-update
1024
361
 
1025
- The proxy checks npm for a newer version on startup and every 3 minutes. When a
1026
- newer version is found it installs it in place (tarball over the install dir)
1027
- and logs a notice — **restart `bili` to pick up the new version**.
1028
-
1029
- While the running process is behind the on-disk install ("stale"), the state is
1030
- visible without digging through logs:
1031
-
1032
- - The web UI (`/__bili/`) shows a banner on the overview page: which version is
1033
- running vs installed, and whether auto-restart is enabled.
1034
- - `GET /__bili/status` returns `{version, diskVersion, stale,
1035
- autoRestartOnUpdate, inFlight}` for scripting.
1036
- - A one-time `[update] … restart bili to activate` warning per version pair
1037
- stays in the log.
1038
-
1039
- **Opt-in self-restart.** With `--auto-restart-on-update` (or env
1040
- `ACP_AUTO_RESTART_ON_UPDATE=1`, or `"autoRestartOnUpdate": true` in the config
1041
- file — default OFF) the proxy re-execs itself instead of waiting for a human:
1042
- when the on-disk version is newer and there are **zero in-flight requests**, it
1043
- verifies the new install, stops accepting connections, drains, spawns a
1044
- replacement process on the same port, waits until it accepts connections, then
1045
- exits. Clients reconnect to the same port automatically; session state survives
1046
- (persisted on disk). Safety gates: zero in-flight at decision time *and*
1047
- through the drain window; an install sanity check before re-exec; a 10-minute
1048
- cooldown marker so a flapping version can never loop-restart. Any failure
1049
- resumes the original listener and falls back to the plain reminder.
1050
-
1051
- Disable permanently via config (`"autoUpdate": false`) or env
1052
- (`ACP_AUTO_UPDATE=0`).
362
+ The proxy checks npm on startup and every 3 minutes; a newer version is installed in place and a notice is logged — **restart `bili` to pick it up**, unless you enable opt-in self-restart (`--auto-restart-on-update` flag, env `ACP_AUTO_RESTART_ON_UPDATE=1`, or `"autoRestartOnUpdate": true` in config — default OFF): with zero in-flight requests it verifies the new install, stops accepting connections, drains, spawns a replacement on the same port, and exits once it accepts connections (clients reconnect automatically; session state survives on disk). Safety gates: zero in-flight through the drain window, an install sanity check before re-exec, and a 10-minute cooldown marker so a flapping version can never loop-restart; any failure resumes the original listener and falls back to the plain reminder.
363
+
364
+ While the running process is behind the on-disk install ("stale"), the web UI shows a banner (running vs installed version, auto-restart state) and `GET /__bili/status` returns `{version, diskVersion, stale, autoRestartOnUpdate, inFlight}` for scripting. Disable permanently via `"autoUpdate": false` in config or `ACP_AUTO_UPDATE=0`.
1053
365
 
1054
366
  ## Configuration
1055
367
 
@@ -1057,114 +369,14 @@ The full configuration reference — config file location, top-level keys,
1057
369
  providers, compression tuning, environment variables — lives in
1058
370
  **[CONFIGURATION.md](CONFIGURATION.md)**.
1059
371
 
1060
- ### Upstream proxy (firewall / GFW)
1061
-
1062
- If the proxy's own outbound connections to a model provider are blocked
1063
- (e.g. `api.openai.com` from inside the GFW), configure an **upstream proxy**
1064
- (the local v2rayA / clash HTTP port) so the proxy reaches the provider:
1065
-
1066
- ```jsonc
1067
- {
1068
- // Global default: ALL providers route through this proxy
1069
- "proxy": "http://127.0.0.1:20172",
1070
- "providers": {
1071
- "https://api.openai.com/v1": {
1072
- // Per-URL overrides global (use a different proxy for this host)
1073
- "proxy": "http://127.0.0.1:20173",
1074
- "models": { "gpt-5": { "context": 400000 } }
1075
- },
1076
- "https://open.bigmodel.cn/api/anthropic": {
1077
- // Empty string = explicitly DIRECT, overriding the global proxy
1078
- "proxy": "",
1079
- "models": { "glm-5.2": { "context": 1000000 } }
1080
- }
1081
- }
1082
- }
1083
- ```
1084
-
1085
- Rules:
1086
- - **Per-URL `proxy`** has the highest priority for its matching provider URL.
1087
- - Remaining priority is `BILI_UPSTREAM_PROXY` → Web UI manual proxy → top-level
1088
- `proxy` → `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` → Windows system proxy
1089
- → direct.
1090
- - Empty string `""` means **explicitly direct** (override-and-disable).
1091
- - Auto mode honors `NO_PROXY` and the Windows proxy bypass list for
1092
- environment/system fallbacks. A proxy pointing back to bili's own local port
1093
- is ignored or rejected to prevent a loop.
1094
- - HTTP and HTTPS proxy origins are supported. SOCKS5 (`socks5`/`socks5h`) is
1095
- not supported: an explicit `BILI_UPSTREAM_PROXY` / config `proxy` with such
1096
- a scheme fails startup with an actionable error, while env/system proxies
1097
- (`HTTPS_PROXY`, …) with such a scheme are ignored with a one-time warning
1098
- (traffic then falls through to direct). For Clash/mihomo, point bili at the
1099
- same mixed port over `http://` (e.g. `http://127.0.0.1:7890`).
1100
- - Both outbound paths are covered: `/bili/` path-mode (fetch) AND MITM CONNECT
1101
- tunnels (the proxy's connection to the real upstream goes through the HTTP
1102
- CONNECT proxy).
1103
- - The auto-updater's own egress (npm registry check + tarball download) uses
1104
- the same decision for its hosts, so `bili update` and auto-update work on
1105
- hosts where npm is only reachable through the proxy (#609).
1106
-
1107
- Env override: `BILI_UPSTREAM_PROXY=http://127.0.0.1:20172` (higher priority than
1108
- the config file). On Windows, common Clash/Mihomo static system proxies are
1109
- discovered automatically; the Web UI shows the effective source and any PAC
1110
- URL detected in Internet Settings.
1111
-
1112
- **MITM vs `/bili/` — distinguishing the key scheme.** A login client
1113
- (ZCode via MITM) and an API-key client can both hit the same host
1114
- (`open.bigmodel.cn`). To let their config differ, MITM traffic uses a
1115
- `mitm://` scheme in the lookup key while `/bili/` traffic uses the real
1116
- `https://`:
1117
-
1118
- | Client | Lookup key example |
1119
- |---|---|
1120
- | ZCode (MITM, login) | `mitm://open.bigmodel.cn` |
1121
- | API-key client (`/bili/`) | `https://open.bigmodel.cn/api/anthropic` |
1122
-
1123
- So you can give ZCode its own proxy without affecting API-key clients:
1124
- ```jsonc
1125
- {
1126
- "providers": {
1127
- "mitm://open.bigmodel.cn": { "proxy": "http://127.0.0.1:20173" },
1128
- "https://open.bigmodel.cn/api/anthropic": { "proxy": "http://127.0.0.1:20172" }
1129
- }
1130
- }
1131
- ```
372
+ Two knobs people look for first:
1132
373
 
1133
- ### Wire-compat role rewrite (`compat.roles`)
1134
-
1135
- Some upstreams reject the `developer` role newer codex clients send on the
1136
- Responses API (`400 Invalid role: developer`). `compat.roles` maps roles to
1137
- what the upstream accepts — applied at the forward boundary to the final
1138
- `openai`/`responses` body (client-sent roles **and** bili's own injected
1139
- prompt alike), global or per-provider, default off = byte-for-byte:
1140
-
1141
- ```jsonc
1142
- {
1143
- "compat": { "roles": { "developer": "system" } },
1144
- "providers": {
1145
- "https://picky.example.com": { "compat": { "roles": { "developer": "user" } } }
1146
- }
1147
- }
1148
- ```
1149
-
1150
- **No configuration needed for the common case.** When an upstream answers a
1151
- request with `400 Invalid role: …`, bili auto-rewrites the offending role to
1152
- `system`, retries the request once, and — if the retry succeeds — remembers
1153
- the mapping **for that session only** (nothing is written to your config).
1154
- Later requests in the session skip the 400 round-trip. The log line printed
1155
- when the auto-fix fires includes a copy-paste per-provider snippet if you
1156
- want the mapping permanently.
374
+ - **Upstream proxy (firewall/GFW)** — routing the proxy's own outbound traffic through v2rayA/clash: full resolution order, empty-string = explicit direct, SOCKS5 rejection, both egress paths, and the `mitm://` vs `https://` key schemes live in [CONFIGURATION.md](CONFIGURATION.md) (Server Settings → `proxy`; Providers → key schemes).
375
+ - **Wire-compat role rewrite (`compat.roles`)** — an upstream that rejects the `developer` role? Covered by [CONFIGURATION.md](CONFIGURATION.md) (Server Settings → `compat`) — including the learn-on-failure auto-fix that needs no configuration at all.
1157
376
 
1158
377
  ## How sessions work
1159
378
 
1160
- The proxy needs a stable per-conversation identifier to isolate compression
1161
- state across concurrent users/accounts. It uses **the conversation value the
1162
- client itself provides, verbatim** (`src/session-id.ts`) — no hashing and no
1163
- protocol/upstream/API-key dimensions. Those dimensions were mutable mid-
1164
- conversation (credentials rotate, users switch relays, the wire protocol can
1165
- change), so keying on them orphaned state exactly when the user kept talking
1166
- (#280, #286). The id is used only inside the proxy (state store, persistence,
1167
- UI label); it is never sent upstream.
379
+ The proxy keys compression state on **the conversation value the client itself provides, verbatim** (`src/session-id.ts`) — no hashing, no protocol/upstream/API-key dimensions (those mutate mid-conversation and orphaned state exactly when users kept talking, #280/#286). The id stays inside the proxy (state store, persistence, UI label); it is never sent upstream.
1168
380
 
1169
381
  Where the value comes from, first hit wins: the plugin's
1170
382
  `x-bili-plugin-conversation` (honored only alongside the `x-bili-plugin`
@@ -1184,22 +396,7 @@ Responses/OpenAI/Anthropic wires.
1184
396
  | **omp** (via plugin) | ✅ yes | `prompt_cache_key` promoted over any fingerprint (#268) |
1185
397
  | **pi** (bare) | ❌ no | nothing → anonymous prefix affinity below |
1186
398
 
1187
- **Header-less clients (pi-like): anonymous prefix affinity.** When a client
1188
- sends no conversation signal at all, the proxy resolves the session from the
1189
- replayed history itself (`src/prefix-affinity.ts`, #309): an incoming request
1190
- reattaches to a stored session only when its history reproduces that
1191
- session's message chain byte-exactly from position 0; otherwise it gets its
1192
- own deterministic `pfa-…` session. Consequences for the failure modes this
1193
- section used to warn about (#1262):
1194
-
1195
- - A **resumed** conversation reattaches to its own session — including after
1196
- a proxy restart (#499).
1197
- - A **new task with an identical opener does NOT inherit** another
1198
- conversation's blocks or protected zone: it mints a fresh session, and once
1199
- its history diverges it is fully separate (fork lineage is recorded for
1200
- debugging).
1201
- - A request with no usable signal at all is rejected with an explicit 400
1202
- instead of silently colliding with something else's state.
399
+ **Header-less clients (pi-like): anonymous prefix affinity.** With no conversation signal at all, the proxy resolves the session from the replayed history itself (`src/prefix-affinity.ts`, #309): a request reattaches to a stored session only when its history reproduces that session's message chain byte-exactly from position 0; otherwise it gets its own deterministic `pfa-…` session. Consequences (#1262): a **resumed** conversation reattaches to its own session (even after a proxy restart, #499); a **new task with an identical opener does NOT inherit** another conversation's blocks — it mints a fresh, fully separate session; a request with no usable signal at all gets an explicit 400 instead of silently colliding.
1203
400
 
1204
401
  Design record and threat model: [SESSION-IDENTITY.md](SESSION-IDENTITY.md). The
1205
402
  message-granularity sibling (why identity is content-derived, not an
@@ -1209,87 +406,19 @@ For upstream sticky-routing, the proxy forwards only identity values the
1209
406
  client already supplied (e.g. a body `session_id` is forwarded upstream as
1210
407
  `x-session-id`); it never synthesizes one itself.
1211
408
 
1212
- **Recommendation:** clients that send an explicit id are safe to run many
1213
- concurrent conversations through the proxy. For header-less multi-agent use,
1214
- prefer the client plugin (the omp/pi plugins stamp a stable id per
1215
- conversation); otherwise pass an explicit `x-acp-session` header per
1216
- conversation. Even without either, prefix affinity keeps distinct tasks apart
1217
- — the cost of a diverged fork is one raw resend plus a compression-ladder
1218
- restart.
409
+ **Recommendation:** explicit-id clients are safe to run concurrently. For header-less multi-agent use, prefer the client plugin (stamps a stable id per conversation) or pass an explicit `x-acp-session` header — otherwise prefix affinity still keeps distinct tasks apart (a diverged fork costs one raw resend plus a compression-ladder restart).
1219
410
 
1220
411
  ### Derived (child) sessions inherit the parent's compressed context (#1333, #1362)
1221
412
 
1222
- When an agent spawns a child session — a subagent or fork that starts from an
1223
- empty history instead of resending the parent's conversation — that child
1224
- could not previously `decompress` or `search_context` content that was folded
1225
- away in the parent. Each lane now reports the lineage at birth: its identity
1226
- registration carries the parent's conversation id (`parentConversationId`),
1227
- and the proxy records a read-only link (`derivedFrom`) on the child session.
1228
- From then on:
1229
-
1230
- - `decompress` / `search_context` fall back along the parent chain for
1231
- content the child never saw itself (resident or on-disk parents,
1232
- cycle-guarded, depth cap 8);
1233
- - nothing is copied into the child's state and the parent is never modified —
1234
- fallback hits are read-only, so a child can never clobber what the parent
1235
- still owns;
1236
- - if the parent is unknown to the proxy when the link is recorded, the child
1237
- simply starts fresh.
1238
-
1239
- | Lane | Parent signal |
1240
- |---|---|
1241
- | **pi** RLM inline spawn | `parentSession` in the session header (path to the parent session file, resolved to its session id) |
1242
- | **omp** fork / newSession | `parentSession` in the session header (bare session id or file path — both accepted) |
1243
- | **OpenCode V1** (native plugin) | SDK session info `parentID` (resolved once per session, cached) |
1244
- | **OpenCode V2** (native plugin) | `session.created` event `data.parentID` |
1245
-
1246
- claude/codex/dsh need nothing here: they share one session id across
1247
- subagents or have no child-session concept at all.
413
+ A child session (subagent/fork starting from an empty history) reports its lineage at birth — its identity registration carries `parentConversationId` and the proxy records a read-only `derivedFrom` link — so `decompress` / `search_context` fall back along the parent chain for content the child never saw itself (cycle-guarded, depth cap 8). Nothing is copied into the child, the parent is never modified; if the parent is unknown the child simply starts fresh. Per-lane parent signals (pi/omp `parentSession` header, OpenCode V1/V2 `parentID`): [SESSION-IDENTITY.md](SESSION-IDENTITY.md#derived-child-sessions-lineage-at-birth-1333-1362). claude/codex/dsh need nothing here: they share one session id across subagents or have no child-session concept at all.
1248
414
 
1249
415
  ### Windows: exclude the sessions dir from antivirus (#362)
1250
416
 
1251
- The proxy persists each session's compression state to the sessions dir
1252
- (`%USERPROFILE%\.local\share\billion-context\` by default) and rewrites the
1253
- file every turn of a long session. Persisted per session: the compression
1254
- state (block summaries), the compressed originals cache (`blockContents`,
1255
- what `bili export --full` recovers), and a bounded folded-view snapshot of
1256
- the recent conversation (newest `BILI_PERSIST_TAIL_TOKENS` tokens, default
1257
- 16k) — the raw full history is never duplicated on disk (#401). On
1258
- Windows, real-time antivirus (Windows
1259
- Defender), the search indexer, or a sync tool (OneDrive) can lock that
1260
- directory mid-write, so the rename fails with `EPERM` and every persist for
1261
- that session fails until the lock clears.
1262
-
1263
- When the same session fails N consecutive writes (default `5`), the proxy
1264
- logs a one-time, actionable alert naming the directory to exclude. To fix it
1265
- at the root: add `%USERPROFILE%\.local\share\billion-context\` to your
1266
- antivirus **exclusions** (Windows Defender: Settings → Virus & threat
1267
- protection → Manage settings → Exclusions → Add an exclusion → Folder) and
1268
- make sure no sync tool (OneDrive / Dropbox / …) is syncing that path. Full
1269
- steps in [CONFIGURATION.md](CONFIGURATION.md#windows-exclude-the-sessions-dir-from-antivirus-362).
417
+ The proxy rewrites each session's state file every turn of a long session; on Windows, real-time AV (Defender), the search indexer, or a sync tool (OneDrive) can lock the sessions dir mid-write, so persists fail with `EPERM` until the lock clears. Fix at the root: add `%USERPROFILE%\.local\share\billion-context\` to your antivirus exclusions (Defender steps included) and keep sync tools off that path — [CONFIGURATION.md](CONFIGURATION.md#windows-exclude-the-sessions-dir-from-antivirus-362).
1270
418
 
1271
419
  ### Session file cleanup (#1082)
1272
420
 
1273
- Short-lived sessions leave small state files behind that are never resumed.
1274
- Cleanup is **opt-in** — set `BILI_SESSION_GC=1` to enable it (off by default:
1275
- session files are user data, so there is no silent deletion policy). When
1276
- enabled and persistence is on, bili sweeps the sessions dir at boot and
1277
- hourly, and deletes a file only when BOTH hold: it is older than
1278
- `BILI_SESSION_GC_MAX_AGE_DAYS` (default 7 days), AND the session was never
1279
- compressed (no folded blocks) with its newest request body ≤
1280
- `BILI_SESSION_GC_MAX_TOKENS` tokens (default 1M; unrecorded legacy files use
1281
- `contextTokens`) — so deletion loses nothing but bytes: resuming rebuilds the
1282
- context from the client's own history at the cost of one cold rebuild.
1283
- CCR content stores (#1097) live next to their session file as
1284
- `<hash>.content-store.json` and follow the same lifecycle (#1180): a store is
1285
- deleted together with its session file, an orphaned store (session file
1286
- already gone) is swept once past the age gate, and the store's token footprint
1287
- (unique-content counted with the kernel's CJK-aware `defaultCountTokens` — the
1288
- same estimator as `rawInputTokens`) counts toward the size ceiling above. Compressed
1289
- sessions are never deleted (their summaries cannot be rebuilt losslessly). Every deletion is audit-logged individually, plus one summary
1290
- line per non-empty sweep. Live sessions, unreadable files, and encrypted
1291
- files are handled conservatively (decoded via `BILI_ENCRYPTION_KEY` before
1292
- judging). Details in [CONFIGURATION.md](CONFIGURATION.md#environment-variables).
421
+ Short-lived sessions leave small state files behind that are never resumed. Cleanup is **opt-in** (`BILI_SESSION_GC=1`; off by default — session files are user data, no silent deletion policy): when enabled, bili sweeps the sessions dir at boot and hourly and deletes a file only when BOTH hold — older than `BILI_SESSION_GC_MAX_AGE_DAYS` (default 7 days) AND never compressed with its newest request body ≤ `BILI_SESSION_GC_MAX_TOKENS` tokens (default 1M) — so deletion loses nothing but bytes. CCR content stores follow their session file's lifecycle; compressed sessions are never deleted; every deletion is audit-logged. Full policy: [CONFIGURATION.md](CONFIGURATION.md#environment-variables).
1293
422
 
1294
423
  ## Status
1295
424