billion-context-dsh 0.2.20 → 0.2.22

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.en.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [English](./README.en.md) | [中文](./README.md)
4
4
 
5
5
  > **⚠️ Beta notice — not for production use**
6
- > This project (**v0.2.20**) is a work-in-progress beta. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) itself is also in **public beta**. **Do not use either in engineering / production environments** — expect breaking changes and rough edges.
6
+ > This project (**v0.2.22**) is a work-in-progress beta. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) itself is also in **public beta**. **Do not use either in engineering / production environments** — expect breaking changes and rough edges.
7
7
 
8
8
  <p align="center">
9
9
  <strong>Built with gratitude on top of these projects</strong> — please give them a ⭐:
@@ -68,24 +68,26 @@ The command installs the package and automatically layers this package's bundle
68
68
 
69
69
  Restart `dsh` afterwards (bundle layers are composed at startup), open a new session, and verify: ask the model to call `acp_status`, or run `/acp status`. Shipped presets (standard / code / cordis) keep their realm-local `compaction-basic` fallback (automatic pressure compression still runs there; the ACP tools and nudge coexist); minimal and presets without a compaction realm use this engine directly.
70
70
 
71
- > **DSH version compatibility.** The package declares all four runtime seam
72
- > packages (`dsh-compaction` / `dsh-session` / `dsh-llm` / `dsh-tools`) as peer
73
- > dependencies, sharing the range
74
- > `^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.4`,
75
- > covering the `0.1.0-rc.x` and `0.1.1-rc.x` release lines (including the current
76
- > DSH release; the seam's `src/` is unchanged from `0.1.0-rc.6` to `0.1.1-rc.2`, so the public
77
- > API is identical) and the `0.1.2-alpha.x` line (which removed the
78
- > `Session.events` getter in favour of `snapshotEvents()` / `eventAt()`; this
79
- > engine feature-detects both shapes, so one build runs on either seam). The
80
- > range is multiple `||` clauses **on purpose**: npm
81
- > (node-semver) only lets a prerelease version satisfy a range that carries a
82
- > comparator on the SAME `[major, minor, patch]` tuple as the candidate, so a lone
83
- > `^0.1.0-rc.6` can never match `0.1.1-rc.x` (issue #68) or `0.1.2-alpha.x` —
84
- > older releases fail to install on DSH 0.1.1-rc.x / 0.1.2-alpha.x; upgrade to a
85
- > release containing this fix. Declaring all four seam packages as peers (not
86
- > just `dsh-compaction`) ensures that, even under pnpm's
71
+ > **DSH version compatibility.** The package declares all five runtime seam
72
+ > packages (`dsh-compaction` / `dsh-session` / `dsh-llm` / `dsh-tools` /
73
+ > `dsh-settings`) as peer
74
+ > dependencies, sharing the range `>=0.1.5-alpha.1 <0.1.6-0` — exactly the
75
+ > `0.1.5` line (every prerelease plus the final `0.1.5`). From the `0.1.5` line
76
+ > on, the session's replace operation was renamed from `{ op, start, end }` to
77
+ > `{ op, startSeq, endSeq }` and is validated strictly (exactly those three
78
+ > keys), so the engine emits the new shape only: on older DSH hosts (< 0.1.5)
79
+ > every `compress` call is rejected at runtime (issue #136), which is why the
80
+ > old lines are out of contract — upgrade DSH before installing this release.
81
+ > The explicit bounds (instead of a caret) are deliberate: a caret would
82
+ > silently admit the unverified 0.1.6+ line. Declaring all five seam packages
83
+ > as peers (not just `dsh-compaction`) ensures that, even under pnpm's
87
84
  > hoisted/linked layout, installations resolve them to the **host's own** copy
88
85
  > rather than a stale nested copy inconsistent with the host.
86
+ >
87
+ > Behavior note (from 0.1.5 on): the host no longer permits invisible
88
+ > replacement nodes, so when the engine cleans up orphaned tool messages it
89
+ > leaves one short visible placeholder message in their place; the host-owned
90
+ > system prompt node (surface node 0) is excluded from compressible ranges.
89
91
 
90
92
  **Path B: plain `npm install` (package only — a composition row is required).**
91
93
 
@@ -98,7 +100,7 @@ This only installs the package into your project/global store; it does **not** t
98
100
  **Install from the git source (`github:` spec — the form the plugin store shows).** The prebuilt `dist/` artifacts are committed to this repository, so a git-source install also works out of the box — **no build step needed**, and pnpm 11's default build-script blocking (`allowBuilds`) never applies to this package:
99
101
 
100
102
  ```bash
101
- dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.20
103
+ dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.22
102
104
  ```
103
105
 
104
106
  Prefer a `#<tag>` ref to get artifacts identical to that npm release; without a ref you get the latest default-branch build. Only building the repo yourself (`npm run build`) requires approving build scripts. Background and trade-offs: [docs/git-source-install-design.md](docs/git-source-install-design.md) (issue #92).
@@ -149,6 +151,24 @@ Two audiences: ① Path B (plain npm install) users, who must write a compositio
149
151
 
150
152
  See [docs/configurable-prompts-design.md](docs/configurable-prompts-design.md) for the full slot list, per-slot placeholders, and the empty-string/`null` semantics. Deployments that omit `prompts` use the kernel rendering directly (aligned with kernel/pi; see design doc v6).
151
153
 
154
+ **(Optional) Runtime settings — edit `~/.dsh/settings.yaml` or use `/acp config`; no restart.** Six scalar keys (`modelContextLimit`, `autoModelContextLimit`, `nudgeMinContextLimitPct`, `nudgeMaxContextLimitPct`, `nudgeEmergencyThresholdPct`, `autoNudge` — the rows marked “runtime-adjustable” in the Configuration table) have a hot-editable copy in the host settings layer: editing the settings file or `/acp config` takes effect **immediately on running sessions** (the composition-row `config:` stays the starting point — the layering is schema default → composition row → user settings section):
155
+
156
+ ```yaml
157
+ # ~/.dsh/settings.yaml
158
+ compaction-acp:
159
+ nudgeMaxContextLimitPct: 0.72 # saved → live, no restart
160
+ ```
161
+
162
+ ```text
163
+ /acp config # list the six keys + source layer (user / base / default)
164
+ /acp config set nudgeMaxContextLimitPct 0.72 # change one key live
165
+ /acp config set autoNudge false # boolean keys accept false
166
+ /acp config reset nudgeMaxContextLimitPct # back to the composition row / engine default
167
+ /acp config reset all
168
+ ```
169
+
170
+ Changing a window key (`modelContextLimit` / `autoModelContextLimit`) clears the window-probe cache — the next pre-step re-probes under the new values (probe failures are cached too, so this is also how a fixed gateway gets re-probed). In provider-less plain-npm compositions `/acp config` degrades to advice text; `settingsEnabled: false` disables the integration entirely (composition-row-only — the switch is deliberately NOT part of the settings layer: it cannot turn itself off). Design details: [docs/settings-integration-design.md](docs/settings-integration-design.md).
171
+
152
172
  **Per-mode — an agent preset's `compaction` realm.** First *disable (or delete) the realm's existing `dsh-compaction-basic` row*, then mount this engine — two backends cannot coexist in the same realm:
153
173
 
154
174
  ```yaml
@@ -175,11 +195,13 @@ DSH derives every model request from its append-only session log (the *surface*)
175
195
  | refs (`m00001` tags) | surface seqs, carried by the nudge's compressible-range table |
176
196
  | nudge ("efficiency note — compress early and keep context lean") | injected at `agent/pre-step` by the kernel's pressure decision — efficiency note + context breakdown + compression rules, tone aligned with kernel/pi; never an order |
177
197
  | `decompress` | read-only recovery of shadowed originals from the log |
178
- | `search_context` | scores a unified doc set (block summaries + shadowed originals) rebuilt from the log via acp-kernel `searchBlocks` (hybrid: stemming + CJK bigrams + char n-gram fuzzy); hits link back to the owning block |
198
+ | `search_context` | scores a unified doc set (block summaries + shadowed originals) rebuilt from the log — cached per log snapshot, so repeated searches before the next append reuse it (issue #133) — via acp-kernel `searchBlocks` (hybrid: stemming + CJK bigrams + char n-gram fuzzy); hits link back to the owning block |
179
199
  | `acp_status` | CONTEXT BREAKDOWN (tool/text/summaries shares of the visible total) + compressed-block ledger + nudge decision line + a `Checkpoint seqs` row mapping each ACTIVE block's kernel ref (`bN`) to its checkpoint summary seq — compressing a checkpoint seq distills that block (issue #60); no context-window rows; scope/view/tool/sort/limit drilldown supported |
180
200
  | block state | in-memory kernel state + **log-rebuilt ledger** (no sidecar files) |
181
201
  | tiered distillation (T2/T3) | re-compressing a block's summary node distills that block (tier 2); distilling a tier-2 block yields tier 3. Tier + kernel block ids are persisted to the log, so kernel state rehydrates from the log after a restart and stays distillable |
182
- | compression accounting (shadow price) | `shadowedTokenCount` (what the host occupancy display deducts) is priced in the **host token-meter's vocabulary** (`ctx.tokenMeter.measure` preferred; exact mirror in `src/host-tokens.ts` as fallback) — never the plugin's internal CJK-aware estimate (that is display currency; mixing it into the host ledger can drive `messageTokens` negative and brick a CJK-heavy session, issue #54) |
202
+ | compression accounting (shadow price) | `shadowedTokenCount` (what the host occupancy display deducts) is priced with the **host token-meter's fixed-heuristic price** (`ctx.tokenMeter.measure` preferred, reading the `heuristicTokens ?? tokens` fixed-heuristic basis; exact mirror in `src/host-tokens.ts` as fallback) — never the plugin's internal CJK-aware estimate (that is display currency; mixing it into the host ledger can drive `messageTokens` negative and brick a CJK-heavy session, issue #54), and never the route-repriced `node.tokens` either (under 0.1.2+ image route pricing that is request-pressure currency; summing it overstates an image range's claim and folds the same ledger negative, issue #103) |
203
+ | image/file block visibility | `image`/`file` blocks carry no characters, and the projection used to drop them silently — so they had no ref in the compressible-range table, could not act as a boundary, were invisible to the kernel's "last user message" protection, and were priced at 0 tokens (on a screenshot session the last question could be compressed away entirely; issue #117). `extractText` now renders a deterministic one-line placeholder from durable attachment metadata (`[image image/png shot.png 800x600 4.2KB]`, `[file notes.txt 1.0KB]` — the same idea as the host's own handle-text projection for files), the media price is the difference between the host token-meter node's two prices (`tokens` − `heuristicTokens`: the second is the route-independent fixed heuristic, the first carries the adapter's declared visual price for a media occurrence, so the difference is exactly what the route charged on top) **plus** the fixed structural price the host heuristic charges for a media reference (`hostMediaStructuralPrice`, a mirror of the host's own `estimateStructuralBlock`) — no adapter declares a visual price today, so the difference alone would leave pictures looking free again, and range rows carry `[+N images]`; a session with no media pays nothing extra |
204
+ | injected-instruction hygiene | the host injects policy files (AGENTS.md etc.) onto the session surface; compressing the **current copy** of one makes the host re-inject the same file immediately — a compress → re-inject → compress loop (issue #71). The compressible-range table treats injected rows as barriers: never offered, never part of a span; a manual compress — the model's compress tool AND the human /acp compress command alike — that covers a file's current copy is **rejected outright** (the error names the seq(s) and points out that stale copies are fine to compress) — compressing a current copy reclaims nothing because the host always re-pastes it. Rows are grouped by `source.changes[].scope` (= one file) and only each group's newest copy is protected — older copies superseded by an update stay safely compressible |
183
205
 
184
206
  The load-bearing compression guidance (tools, philosophy, summary rules, tier rules) is registered as a one-time system-prompt section; each nudge carries a condensed version (efficiency note + philosophy + context breakdown + HOW_TO_COMPRESS_RULES + range table + batch tip). There is deliberately **no automatic summarization**: automatic policy only nudges the model (`compactIfNeeded` returns null).
185
207
 
@@ -193,12 +215,15 @@ A walkthrough of the ACP philosophy this project inherits — how active context
193
215
 
194
216
  | Tool | What it does |
195
217
  | --- | --- |
196
- | `compress` | Replace a seq range with a dense summary you write (edges auto-balanced to tool-pair boundaries); re-compressing a block's summary node distills it (tier 2/3) |
197
- | `decompress` | Restore a previously compressed block's original content (read-only); accepts the `bN` ref shown by acp_status or a compaction id |
218
+ | `compress` | Replace a seq range with a dense summary you write (edges auto-balanced to tool-pair boundaries); re-compressing a block's summary node distills it (tier 2/3). Each range may carry `verifiedReadings: string[]` — the files/sections you actually read and verified in that range; persisted with the summary and echoed back in the compress result as `verified: …`, so later turns know what was checked without re-reading |
219
+ | `decompress` | Restore a previously compressed block's original content (read-only); accepts the `bN` ref shown by acp_status or a compaction id. Large blocks are paged by size — each page stays under the host tool-result trim budget (~7K chars, up to 100 messages) — so a normal page comes back intact; pass `offset`/`limit` and follow the continue hint in the result to walk a very large block |
198
220
  | `search_context` | Search compressed block summaries and originals by keyword (acp-kernel hybrid retrieval: stemming + CJK bigrams + fuzzy); hits link back to the owning block |
199
221
  | `acp_status` | CONTEXT BREAKDOWN (tool/text/summaries shares of the visible total) + compressed-block ledger + nudge decision line + a `Checkpoint seqs` row mapping each ACTIVE block's kernel ref (`bN`) to its checkpoint summary seq (the distill entry point, issue #60); no context-window rows. Drilldown supported: `scope:"compressed"` per block, `scope:"uncompressed"` + `view:"messages"`/`"ranges"` per message/range, with `tool` filter, `sort` order and `limit` cap. Drilldown row refs are kernel ids (mN) — feed them straight to `compress` as `startSeq`/`endSeq` (auto-mapped to the live surface seq); `Surface:` seqs work too |
200
222
  | `/acp` | status / compress / decompress from the command bar; status also shows human-side window info (estimated context, window source, compressed-block ledger, and **nudge arbitration** — `nudge: idle/ACTIVE — reason` plus how many tokens remain until the next nudge, decided by the same kernel turn as the nudge path) |
201
223
 
224
+ - **Summary source framing**: every compaction summary is prefixed at write time with `[Model-written summary — not user words; re-verify any obligations before relying on them]` — written into BOTH the durable summary event and its checkpoint node (one identical text), with an idempotent projection-time safety net for legacy blocks. Purpose: stop the model from executing obligation-like sentences inside a summary as if they were the user's own words.
225
+ - **Slim nudges**: the compression philosophy/rules sections are no longer repeated in every nudge body (they already live in the system prompt); a nudge carries only the trigger frame + context breakdown + range table. The template path (`config.prompts.nudge`) strips the same sections. Design background: [docs/injection-governance-design.md](docs/injection-governance-design.md).
226
+
202
227
  ## Upstream & credits
203
228
 
204
229
  This project is a **port/derivation** and stands on the shoulders of the following upstream work — all MIT licensed. **Thank you** to [ranxianglei](https://github.com/ranxianglei) and the DeepSeek Harness team for building these projects and making them open source:
@@ -216,17 +241,41 @@ This project reuses `acp-kernel`'s compression core and `billion-context-pi`'s d
216
241
 
217
242
  | Key | Default | Meaning |
218
243
  |---|---|---|
219
- | `modelContextLimit` | auto-detected (fallback `128000`) | Context window used for the kernel's pressure decisions; an explicit value wins and skips detection. When omitted, the host session projection `contextPressure.contextWindow` is read first — the capacity disclosed for the **current real route** (a session that switched models follows automatically, no restart needed) — and the model API is probed only when the projection discloses no window; an explicit value also skips the output-reservation subtraction (the operator owns the denominator) |
220
- | `autoModelContextLimit` | `true` | Resolve the real context window automatically: the host projection first (`windowFor` → `projectedContextWindow`, `src/window.ts`), then the model API probe (`agent.ctx.llm.resolveModelInfo`); both are skipped when `autoModelContextLimit: false`. On probe failure it falls back to the default, and the `/acp` command shows the window source (the `acp_status` model tool carries no window info). A failed probe is surfaced in the host log and the `/acp` panel (`restart to re-probe`) — the failure is cached like a success, so fixing the gateway requires a restart or an explicit `modelContextLimit` before the probe retries. On a successful probe the adapter's per-request output cap (`defaultMaxTokens` — the output reservation the provider guarantees at the end of the window) is SUBTRACTED, so every downstream pressure decision (nudge tiers, truncate, growth) measures usage against the SUSTAINABLE input budget (window − reservation): a 96K window with a 16K cap carries at most 80K of input, and the raw denominator understated usage by cap/window (≈17% there — and the ratio is far higher on short-window models, where the same cap is a quarter or more of the window). When the cap is undisclosed, the limit is explicit, or the probe fails, the raw-window behavior is kept; `/acp status` shows the subtraction (raw − reservation) |
221
- | `nudgeMinContextLimitPct` | kernel default `0.45` | Nudge window lower bound (usage fraction) — validation only; the growth-driven trigger has no percentage floor — same default as billion-context-pi |
222
- | `nudgeMaxContextLimitPct` | engine default `0.70` (kernel/pi default `0.75`) | Over-limit line: above this the nudge fires regardless of growth — deliberately below the host compaction-basic 80% auto-compaction line so the forced nudge fires first; an explicit value wins (a same-name key in `coreOverrides.nudge` outranks it — see below) |
223
- | `nudgeEmergencyThresholdPct` | engine default `0.85` (kernel/pi default `0.95`) | Emergency nudge (bypasses the per-turn dedup) — lowered from `0.95`: at 95% the model has no room to act and the 80% auto-compaction line shadows it; an explicit value wins (a same-name key in `coreOverrides.nudge` outranks it — see below) |
224
- | `coreOverrides` | — | Any other acp-kernel `Config` override (billion-context-pi's `coreOverrides` escape hatch). Merge order: kernel defaults → top-level pct knobs → `coreOverrides.nudge` lands last — same-name keys take its value |
244
+ | `modelContextLimit` | auto-detected (fallback `128000`) | Context window used for the kernel's pressure decisions; an explicit value wins and skips detection. When omitted, the host session projection `contextPressure.contextWindow` is read first — the capacity disclosed for the **current real route** (a session that switched models follows automatically, no restart needed) — and the model API is probed only when the projection discloses no window; an explicit value also skips the output-reservation subtraction (the operator owns the denominator) (runtime-adjustable: `/acp config`) |
245
+ | `autoModelContextLimit` | `true` | Resolve the real context window automatically: the host projection first (`windowFor` → `projectedContextWindow`, `src/window.ts`), then the model API probe (`agent.ctx.llm.resolveModelInfo`); both are skipped when `autoModelContextLimit: false`. On probe failure it falls back to the default, and the `/acp` command shows the window source (the `acp_status` model tool carries no window info). A failed probe is surfaced in the host log and the `/acp` panel (`restart to re-probe`) — the failure is cached like a success, so fixing the gateway requires a restart or an explicit `modelContextLimit` before the probe retries. On a successful probe the adapter's per-request output cap (`defaultMaxTokens` — the output reservation the provider guarantees at the end of the window) is SUBTRACTED, so every downstream pressure decision (nudge tiers, truncate, growth) measures usage against the SUSTAINABLE input budget (window − reservation): a 96K window with a 16K cap carries at most 80K of input, and the raw denominator understated usage by cap/window (≈17% there — and the ratio is far higher on short-window models, where the same cap is a quarter or more of the window). When the cap is undisclosed, the limit is explicit, or the probe fails, the raw-window behavior is kept; `/acp status` shows the subtraction (raw − reservation). Changing `modelContextLimit`/`autoModelContextLimit` via `/acp config` clears the window cache so the probe re-runs immediately |
246
+ | `nudgeMinContextLimitPct` | kernel default `0.45` | Nudge window lower bound (usage fraction) — validation only; the growth-driven trigger has no percentage floor — same default as billion-context-pi (runtime-adjustable: `/acp config`) |
247
+ | `nudgeMaxContextLimitPct` | engine default `0.70` (kernel/pi default `0.75`) | Over-limit line: above this the nudge fires regardless of growth — deliberately below the host compaction-basic 80% auto-compaction line so the forced nudge fires first; an explicit value wins (a same-name key in `coreOverrides.nudge` outranks it — see below) (runtime-adjustable: `/acp config`) |
248
+ | `nudgeEmergencyThresholdPct` | engine default `0.85` (kernel/pi default `0.95`) | Emergency nudge (bypasses the per-turn dedup, but is capped at 3 injections per user turn — issue #108) — lowered from `0.95`: at 95% the model has no room to act and the 80% auto-compaction line shadows it; an explicit value wins (a same-name key in `coreOverrides.nudge` outranks it — see below) (runtime-adjustable: `/acp config`) |
249
+ | `preset` | — | (optional) Pick the nudge aggressiveness in one word: `preserve` / `relaxed` / `balanced` / `efficient` / `aggressive` (see “Presets” below). Fills ONLY the three nudge thresholds you did not set explicitly; precedence is explicit value > `preset` > engine default. An unknown name fails construction, and so does a merged window that ends up inverted (wrong `min` / `max` / `emergency` order). Does not touch any other knob (`modelContextLimit` / `autoNudge` / `prompts` / `coreOverrides`) (composition-only: not yet wired into `/acp config` — a follow-up on issue #75) |
250
+ | `coreOverrides` | — | Any other acp-kernel `Config` override (billion-context-pi's `coreOverrides` escape hatch). Merge order: kernel defaults → top-level pct knobs → `coreOverrides.nudge` lands last — same-name keys take its value (read-only: composition-row-only, not exposed through settings) |
225
251
  | `autoTools` | `true` | Register the four model tools on `ctx.tools` |
226
252
  | `autoCommand` | `true` | Register the `/acp` command on `ctx.commands` |
227
- | `autoNudge` | `true` | Inject the nudge into `agent/pre-step` |
253
+ | `autoNudge` | `true` | Inject the nudge into `agent/pre-step` (runtime-adjustable: `/acp config`) |
254
+ | `settingsEnabled` | `true` (enabled when unset) | (optional) Disable the runtime-settings integration entirely (composition-row-only, deliberately NOT in the settings layer — the switch cannot turn itself off; with it off the composition-row `config:` stays the only effective channel) |
228
255
  | `prompts` | — | (optional) Custom prompt copy: per-slot overrides for nudge / range table / system prompt / tool descriptions (template + named placeholders, validated at construction; see “Custom prompt copy” above and [docs/configurable-prompts-design.md](docs/configurable-prompts-design.md)) |
229
256
 
257
+ ## Presets
258
+
259
+ If you do not want to tune three percentages by hand, pick the nudge aggressiveness in one word — five tiers from “keep context as long as possible” to “compress early and often”:
260
+
261
+ | `preset` | min | max | emergency | Trade-off |
262
+ |---|---|---|---|---|
263
+ | `preserve` | 0.55 | 0.78 | 0.93 | Keep context as long as possible — nudge only close to the limit |
264
+ | `relaxed` | 0.50 | 0.75 | 0.90 | Light-touch compression — nudges a little earlier than preserve |
265
+ | `balanced` | 0.45 | 0.70 | 0.85 | Default balance — the same thresholds the plugin ships with (choosing this changes nothing) |
266
+ | `efficient` | 0.40 | 0.60 | 0.78 | Trim more often — favors low token usage over keeping full history |
267
+ | `aggressive` | 0.30 | 0.50 | 0.70 | Lean context — compresses early and frequently |
268
+
269
+ - **Fills only what you left unset**: `preset` fills ONLY the `nudge*ContextLimitPct` values you did not set explicitly; if you set both a `preset` and one of those thresholds, your explicit value wins (precedence: explicit > preset > default).
270
+ - **No other knob is touched**: `modelContextLimit`, `autoNudge`, `prompts`, and `coreOverrides` are unaffected; `coreOverrides.nudge` still lands last and its same-name keys outrank everything.
271
+ - **See the active tier**: `/acp status` prints the effective `preset` and the three thresholds **actually in force** — the line mirrors `kernelConfigFor`'s merge order, so any explicit override you made on top of the preset AND any same-name key in `coreOverrides.nudge` are shown as they really apply.
272
+ - **A typo fails loudly**: an unknown name throws at engine construction and lists the valid tiers (the same fail-fast contract as custom prompt templates) — it never silently falls back to the defaults. The bundle row itself carries no `config`, so a `preset` can only come from your own same-id `compaction-acp` row; if that row fails to construct, the profile stays down until you fix it (intended fail-fast, not a defect).
273
+ - **Runtime hot-swap**: presets are set at composition time today (install / `cordis.patch.yml`); once #75's settings.yaml hot-reload lands they can be changed from `/acp config`. This PR makes them available at the composition layer first.
274
+ - **Two knobs deliberately left out**: the original request also named `growthRatio` (exists in acp-kernel as `nudge.growthRatio`, reachable via `coreOverrides`) and `protectedLastMessages` (≈ kernel `preserveRecentMessages`). Neither is a first-class engine knob here; adopting them as named keys / UI items is an owner decision, so they are not baked into the presets.
275
+ - **An inverted window is rejected**: if merging with explicit thresholds produces `min > max`, `max > emergency` or `min > emergency` (e.g. `preset: 'preserve'` with `nudgeMaxContextLimitPct: 0.5`), the engine throws at construction and lists the three values. The kernel only warns about such a config, so this check is added by the engine in `resolveAcpConfig`.
276
+ - **`max` is the value that races the host's 80% line**: each tier's onset is decided by `max`; the `preserve` / `relaxed` emergency values (0.93 / 0.90) sit above the host compaction-basic 80% line and act only as a label upgrade past it, so they are never reached when the host compacts first.
277
+ - **`min` has no runtime effect yet**: the pinned kernel reads it only in `validateConfig` (as the `nudgeMinContextLimitPct` option says), so tiers differ in practice by `max` and `emergency`.
278
+
230
279
  ## Development
231
280
 
232
281
  ```bash
@@ -234,8 +283,11 @@ npm install
234
283
  npm run typecheck # strict TS
235
284
  npm test # node --import tsx --test tests/*.test.ts
236
285
  npm run build # tsup bundle (inlines acp-kernel) + .d.ts
286
+ npm run test:e2e # end-to-end host regression: real agent loop + scripted fake LLM (see below)
237
287
  ```
238
288
 
289
+ The end-to-end regression suite (`scripts/e2e/`) assembles the real DSH host in-process (cordis + agent-loop + the DeepSeek adapter), points it at a scripted fake LLM server, mounts this engine as the compaction backend, and asserts the persisted event log: compaction start/end pairing, the durable replace node, strict tool-call/result pairing, and the nudge injection rhythm — plus the request bodies the fake LLM **actually received** (wire-level prompt-cache byte-stability: envelope, `tools` array, leading message, and append-only turns for scenarios without compaction). Background and trade-offs: [docs/e2e-harness-design.md](docs/e2e-harness-design.md) (issue #120).
290
+
239
291
  `dist/index.js` is self-contained except for the `@deepseek-ai/*` seam packages, which the hosting deployment provides.
240
292
 
241
293
  ## Architecture
@@ -246,6 +298,7 @@ src/
246
298
  ├── messages.ts # M1: session events ↔ acp-kernel CoreMessage projection
247
299
  ├── state.ts # M2: per-session kernel state
248
300
  ├── region.ts # M5: durable region transaction + log-rebuilt block ledger
301
+ ├── block-ledger.ts # M5 support: tier/lineage fields encoded inside compaction/summary rawOutput, never top-level (issue #141)
249
302
  ├── tools.ts # M3: compress / decompress / search_context / acp_status
250
303
  ├── nudge.ts # M4: kernel pressure decision → injected advisory nudge
251
304
  ├── system-prompt.ts# M4: one-time ACP guidance section (keeps nudges short)
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [中文](./README.md) | [English](./README.en.md)
4
4
 
5
5
  > **⚠️ 测试版声明——请勿用于生产环境**
6
- > 本项目(**v0.2.20**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
6
+ > 本项目(**v0.2.22**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
7
7
 
8
8
  <p align="center">
9
9
  <strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
@@ -71,20 +71,22 @@ dsh plugin --profile web add billion-context-dsh
71
71
 
72
72
  装完**重启 `dsh`**(bundle 层在启动时组合),新开会话即可用——让模型调用 `acp_status` 或执行 `/acp status` 自证。shipped 预设(standard / code / cordis)内部的 realm 级 `compaction-basic` 自动压缩兜底仍然保留(这些模式里"自动摘要"照旧,ACP 工具与 nudge 并存);minimal 等不带 compaction realm 的预设直接使用本引擎。
73
73
 
74
- > **与 DSH 版本的兼容性。** 包把四个运行期 seam 包(`dsh-compaction` /
75
- > `dsh-session` / `dsh-llm` / `dsh-tools`)都声明为 peer 依赖,共享同一个
76
- > 范围 `^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.4`,同时覆盖 `0.1.0-rc.x`、
77
- > `0.1.1-rc.x` 两条 rc 线与 `0.1.2-alpha.x` 线(从 `0.1.0-rc.6` 到 `0.1.1-rc.2`,
78
- > seam 的 `src/` 源码零改动,公开 API 完全一致;`0.1.2-alpha` 移除了
79
- > `Session.events` getter,改为 `snapshotEvents()` / `eventAt()`,本引擎对两种
80
- > 形态做了特性探测,单一构建两端通用)。范围写成多个并集子句是**有意为之**:
81
- > npm (node-semver)的预发布匹配规则要求 range 里存在与候选版本**相同
82
- > `[major, minor, patch]` 元组**的比较器,单一 `^0.1.0-rc.6` 永远匹配不了
83
- > `0.1.1-rc.x`(issue #68),也匹配不了 `0.1.2-alpha.x`——因此旧发布的包在
84
- > DSH 0.1.1-rc.x / 0.1.2-alpha.x 上装不上,升级到含本次修复的新版本即可。
85
- > 把这四个 seam 包一并声明为 peer(而不只是 `dsh-compaction`),是为了让
74
+ > **与 DSH 版本的兼容性。** 包把五个运行期 seam 包(`dsh-compaction` /
75
+ > `dsh-session` / `dsh-llm` / `dsh-tools` / `dsh-settings`)都声明为 peer
76
+ > 依赖,共享同一个
77
+ > 范围 `>=0.1.5-alpha.1 <0.1.6-0`——恰好是整条 `0.1.5` 线(所有预发布加最终
78
+ > `0.1.5`)。从 `0.1.5` 线起,会话 replace 操作的协议字段由 `{ op, start, end }`
79
+ > 改名为 `{ op, startSeq, endSeq }`,且校验严格(只接受这三个字段);本引擎只
80
+ > 输出新形态,因此在更旧的 DSH(< 0.1.5)上每次 `compress` 都会被宿主在运行时
81
+ > 拒绝(issue #136)——旧版本不再受支持,请先升级 DSH 再安装本发行版。范围
82
+ > 写成显式区间而非 caret 是**有意为之**:caret 会悄悄放进未经验证的 0.1.6+
83
+ > 线。把这五个 seam 包一并声明为 peer(而不只是 `dsh-compaction`),是为了让
86
84
  > 安装在 pnpm 的集成/封存布局下仍能把它们解析到**宿主自己的副本**,而不是
87
85
  > 某个与宿主不一致的陈旧嵌套副本。
86
+ >
87
+ > 行为注记(0.1.5 起):宿主不再允许"不可见"替换节点,引擎清理孤立工具消息时
88
+ > 会在其位置留下一条短可见占位消息;宿主的系统提示节点(surface node 0)被排除
89
+ > 在可压缩范围表之外。
88
90
 
89
91
  **方式二:纯 `npm install`(只装包,需要手写组合行)。**
90
92
 
@@ -97,7 +99,7 @@ npm install billion-context-dsh
97
99
  **git 源安装(`github:` 规格,插件商店展示的形态)。** 预构建产物 `dist/` 已提交到仓库,从 git 源安装同样开箱即用——**无需任何构建步骤**,pnpm 11 默认拦截构建脚本(`allowBuilds`)的机制对这个包不构成障碍:
98
100
 
99
101
  ```bash
100
- dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.20
102
+ dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.22
101
103
  ```
102
104
 
103
105
  建议带 `#<tag>` 安装,拿到与对应 npm 版本完全一致的产物;不带 ref 则装默认分支的最新构建。只有 clone 仓库自行从源码构建(`npm run build`)才需要放行构建。背景与方案取舍见 [docs/git-source-install-design.md](docs/git-source-install-design.md)(issue #92)。
@@ -146,6 +148,24 @@ dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.20
146
148
 
147
149
  可配置槽位清单、每槽可用占位符、空串/`null` 语义见 [docs/configurable-prompts-design.md](docs/configurable-prompts-design.md)。未配置 `prompts` 的部署直接使用 kernel 渲染(对齐 kernel/pi,见设计文档 v6)。
148
150
 
151
+ **(可选)运行时设置 —— 编辑 `~/.dsh/settings.yaml` 或 `/acp config`,无需重启。** 六个标量键(`modelContextLimit`、`autoModelContextLimit`、`nudgeMinContextLimitPct`、`nudgeMaxContextLimitPct`、`nudgeEmergencyThresholdPct`、`autoNudge`,见「配置」表中带「运行时热调」标记的行)在宿主 settings 层有一份可热改的副本:编辑 settings 文件或 `/acp config` 会**立即生效于运行中的会话**(组合行 `config:` 仍是起点——分层为 schema 默认 → 组合行 → 用户 settings 段):
152
+
153
+ ```yaml
154
+ # ~/.dsh/settings.yaml
155
+ compaction-acp:
156
+ nudgeMaxContextLimitPct: 0.72 # 保存即生效,无需重启
157
+ ```
158
+
159
+ ```text
160
+ /acp config # 列出六个键当前值 + 来源层(user / base / default)
161
+ /acp config set nudgeMaxContextLimitPct 0.72 # 热改一个键
162
+ /acp config set autoNudge false # 布尔键(false 是合法值)
163
+ /acp config reset nudgeMaxContextLimitPct # 退回组合行 / 引擎默认
164
+ /acp config reset all
165
+ ```
166
+
167
+ 窗口相关键(`modelContextLimit` / `autoModelContextLimit`)改动会清空窗口探测缓存——下一次 pre-step 按新值重新探测(探测失败也会被缓存,正是靠这个机制在修复网关后重新探测)。无 settings provider 的纯 npm 安装组合下 `/acp config` 降级为指引文案;`settingsEnabled: false` 可整体关闭该集成(组合行专用,不进 settings 层——开关不能关掉自己)。设计细节见 [docs/settings-integration-design.md](docs/settings-integration-design.md)。
168
+
149
169
  **单模式生效(agent preset 的 `compaction` realm)**。先在该 realm 内*禁用(或删除)原有的 `dsh-compaction-basic` 行*,再插入本引擎——同一 realm 内两个后端不能并存:
150
170
 
151
171
  ```yaml
@@ -172,11 +192,13 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
172
192
  | refs(`m00001` 标签) | surface seq,由 nudge 的可压缩范围表携带 |
173
193
  | nudge("效率提示——尽早压缩保持精简") | 由内核的压力决策在 `agent/pre-step` 注入——效率通知 + 上下文分解 + 压缩规则,语气对齐 kernel/pi;绝非命令 |
174
194
  | `decompress` | 从日志只读恢复被遮蔽的原文 |
175
- | `search_context` | 从日志重建块摘要 + 被遮蔽原文的统一文档集,交 acp-kernel `searchBlocks`(hybrid:词干化 + CJK bigram + 字符 n-gram 模糊)打分;命中回链所属块 |
195
+ | `search_context` | 从日志重建块摘要 + 被遮蔽原文的统一文档集(按日志快照缓存:新事件落盘前重复搜索直接复用,issue #133),交 acp-kernel `searchBlocks`(hybrid:词干化 + CJK bigram + 字符 n-gram 模糊)打分;命中回链所属块 |
176
196
  | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行 + `Checkpoint seqs` 行(active 块的 `bN → seq` 映射——压缩某个 checkpoint seq 即蒸馏该块,issue #60);不含上下文窗口;支持 scope/view/tool/sort/limit 钻取 |
177
197
  | 块状态 | 内存内核状态 + **日志重建账本**(无旁车文件) |
178
198
  | 分层蒸馏(T2/T3) | 再次压缩某块的摘要节点 = 蒸馏该块(tier 2),蒸馏 tier-2 块得 tier 3;tier 与内核块 id 持久化进日志,重启后内核状态从日志再水合、可继续蒸馏 |
179
- | 压缩记账(影子价格) | `shadowedTokenCount`(宿主占用率据此扣减)**用宿主 token-meter 的词汇计价**(`ctx.tokenMeter.measure` 优先,`src/host-tokens.ts` 精确镜像兜底)——绝不混用插件内部的 CJK 感知估算(那是展示货币,混用会把宿主账本扣成负数、卡死中文会话,issue #54) |
199
+ | 压缩记账(影子价格) | `shadowedTokenCount`(宿主占用率据此扣减)**用宿主 token-meter 的固定启发价计价**(`ctx.tokenMeter.measure` 优先、按 `heuristicTokens ?? tokens` 读固定启发价基准,`src/host-tokens.ts` 精确镜像兜底)——绝不混用插件内部的 CJK 感知估算(那是展示货币,混用会把宿主账本扣成负数、卡死中文会话,issue #54),也不按路由重定价的 `node.tokens` 计价(0.1.2+ 图片路由计价下那是请求压力价,读它会让含图片区间的 claim 虚报视觉价、同样扣穿账本,issue #103) |
200
+ | 图片/文件块可见性 | `image`/`file` 块没有字符,早先被投影层静默丢弃——于是它们在压缩范围表里没有 ref、不能当边界、不受内核"最近一条 user 消息"保护,还按 0 token 计价(截图会话里最后一条提问可能被整段压掉,issue #117)。现在 `extractText` 用附件的持久元数据生成一行确定性占位符(`[image image/png shot.png 800x600 4.2KB]`、`[file notes.txt 1.0KB]`,与宿主对文件的 handle 文本投影同源),媒体价格读宿主 token-meter 节点上 `tokens` 与 `heuristicTokens` 的差值(该节点只暴露这两个价格:`tokens` 是当前路由下的请求压力,媒体出现时带适配器声明的视觉价,`heuristicTokens` 是与路由无关的固定启发式——差值即路由多收的那部分),并在此之上**叠加宿主固定启发式对媒体引用的结构价**(`hostMediaStructuralPrice`,镜像宿主自己的 `estimateStructuralBlock`):今天所有适配器都不声明视觉价,差值恒为 0,只读差值会让图片重新看起来"免费",范围表行尾标注 `[+N images]`;无媒体的会话不做任何额外测量 |
201
+ | 注入指令行卫生 | 宿主会把 AGENTS.md 等策略文件注入会话表面;压缩其**当前副本**会让宿主立刻重注入同一份文件——压缩→重注→压缩死循环(issue #71)。可压缩范围表把注入行当屏障:永不提供、永不计入段;手动压缩(模型的 compress 工具、人用的 /acp compress 命令)若覆盖某文件的当前副本都会被**直接拒绝**(报错点名行号,并提示过期的旧副本可以压),因为压当前副本没有收益——宿主必重贴。按 `source.changes[].scope`(= 一个文件)分组,只有每组最新副本受保护——被更新取代的旧副本可以安全压缩 |
180
202
 
181
203
  承载性的压缩指引(工具、哲学、摘要规则、tier 蒸馏/浓缩规则)注册为一次性系统提示段;每条 nudge 携带精简版(效率提示 + 哲学 + 上下文分解 + 压缩规则 + 范围表 + 批量提示)。刻意**不做自动摘要**:自动策略只 nudge 模型(`compactIfNeeded` 返回 null)。
182
204
 
@@ -190,12 +212,15 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
190
212
 
191
213
  | 工具 | 作用 |
192
214
  | --- | --- |
193
- | `compress` | 用你书写的紧凑摘要替换 seq 范围(边界自动平衡到 tool-call/result 配对点);对某块的摘要节点再次压缩 = 分层蒸馏(tier 2/3) |
194
- | `decompress` | 恢复已压缩块的原始内容(只读);接受 acp_status 显示的 `bN` 或 compaction id |
215
+ | `compress` | 用你书写的紧凑摘要替换 seq 范围(边界自动平衡到 tool-call/result 配对点);对某块的摘要节点再次压缩 = 分层蒸馏(tier 2/3)。每个 range 可附 `verifiedReadings: string[]`——记录该范围里你实际读过并核实过的文件/章节,随摘要持久化,并在 compress 结果中以 `verified: …` 回显,后续回合不必重读即可知道哪些已核实 |
216
+ | `decompress` | 恢复已压缩块的原始内容(只读);接受 acp_status 显示的 `bN` 或 compaction id。大块按字符预算分页(默认每页约 7K 字符、至多 100 条),使普通页面低于宿主 tool-result 截断阈值(8192);`offset`/`limit` + 续页提示走完全块 |
195
217
  | `search_context` | 按关键词搜索压缩块摘要与原文(acp-kernel hybrid 检索:词干化 + CJK bigram + 模糊);命中回链所属块 |
196
218
  | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行 + `Checkpoint seqs` 行(active 块的 `bN → seq` 蒸馏入口,issue #60);不含上下文窗口。支持钻取:`scope:"compressed"` 逐块、`scope:"uncompressed"` + `view:"messages"`/`"ranges"` 逐消息/区间,`tool` 过滤、`sort` 排序、`limit` 截断。钻取行 ref 是内核 mN——可直接作为 `compress` 的 `startSeq`/`endSeq`(自动映射为 live surface seq);`Surface:` 的 seq 同样可用 |
197
219
  | `/acp` | 从命令栏执行 status / compress / decompress;status 额外展示 human-side 窗口信息(estimated context、context window 来源、压缩账本、**nudge 仲裁**——`nudge: idle/ACTIVE — reason` 及距下一次 nudge 还差多少 token,与 nudge 路径同一内核判定) |
198
220
 
221
+ - **摘要标源(模型自写摘要的框架行)**:每条压缩摘要在写入时前置 `[Model-written summary — not user words; re-verify any obligations before relying on them]`——同时写进持久化摘要事件与其 checkpoint 节点(同一份文本),投影路径对旧块幂等补框。目的:阻止模型把摘要里的义务句当成用户原话直接执行。
222
+ - **瘦身 nudge**:压缩哲学/规则段不再随每次 nudge 重复(它们已在系统提示里);nudge 正文只保留触发框架 + 上下文分解 + 范围表。模板路径(`config.prompts.nudge`)同样摘除这些段落。设计背景:[docs/injection-governance-design.md](docs/injection-governance-design.md)。
223
+
199
224
  ## 上游项目与致谢
200
225
 
201
226
  本项目是一个**移植/派生项目**,站在以下上游工作的肩膀上——全部为 MIT 许可。**衷心感谢** [ranxianglei](https://github.com/ranxianglei) 和 DeepSeek Harness 团队创建并开源这些项目:
@@ -213,17 +238,41 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
213
238
 
214
239
  | 键 | 默认值 | 含义 |
215
240
  |---|---|---|
216
- | `modelContextLimit` | 自动探测(回退 `128000`) | 用于内核压力决策的上下文窗口;显式配置时优先且跳过探测。省略时优先读宿主会话投影 `contextPressure.contextWindow`(按**当前真实路由**披露的新窗口,切模型会话自动跟随,无需重启),无投影时再从模型 API 探测;显式配置同样跳过输出预留扣减(分母完全由操作者定义) |
217
- | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`/acp` 命令展示窗口来源(模型工具 `acp_status` 不含窗口信息)。省略时窗口先读宿主投影(`windowFor` → `projectedContextWindow`,`src/window.ts`)再走探测;投影与探测在 `autoModelContextLimit: false` 时均跳过。探测失败会在宿主日志与 `/acp` 面板提示(`restart to re-probe`)——失败结果同样被缓存,修复网关后需重启或显式设置 `modelContextLimit` 才会重新探测。探测成功后还会**扣减 adapter 的每请求输出上限**(`defaultMaxTokens`,窗口末端每请求保证的输出预留):所有下游压力决策(nudge 档位、truncate、growth)以「可持续输入预算」(窗口 − 输出预留)为分母——96K 窗口 + 16K 上限实际最多承载 80K 输入,原裸窗口分母会把用量低估 cap/window(此处 ≈17%;小上下文窗口模型比例更高);上限未披露、显式配置或探测失败时保持裸窗口行为,`/acp status` 展示扣减明细(raw − reservation) |
218
- | `nudgeMinContextLimitPct` | 内核默认 `0.45` | Nudge 窗口下界(用量占比)——仅作配置校验,增长路径的触发没有百分比下限——与 billion-context-pi 相同的默认值 |
219
- | `nudgeMaxContextLimitPct` | engine 默认 `0.70`(内核/pi 默认 `0.75`) | 过限线:超过此值则无论增长与否都触发 nudge——刻意低于宿主 compaction-basic 的 80% 自动压缩线,保证强制 nudge 先触发;显式配置优先(`coreOverrides.nudge` 同名键优先级更高,见下) |
220
- | `nudgeEmergencyThresholdPct` | engine 默认 `0.85`(内核/pi 默认 `0.95`) | 紧急 nudge(绕过每轮去重)——从 `0.95` 下调:95% 时模型已无操作空间且会被 80% 自动压缩线遮蔽;显式配置优先(`coreOverrides.nudge` 同名键优先级更高,见下) |
221
- | `coreOverrides` | — | 任何其他 acp-kernel `Config` 覆盖(billion-context-pi 的 `coreOverrides` 逃生口)。合并顺序:内核默认 → 顶层 pct 配置 → `coreOverrides.nudge` 最后落地——同名键以它为准 |
241
+ | `modelContextLimit` | 自动探测(回退 `128000`) | 用于内核压力决策的上下文窗口;显式配置时优先且跳过探测。省略时优先读宿主会话投影 `contextPressure.contextWindow`(按**当前真实路由**披露的新窗口,切模型会话自动跟随,无需重启),无投影时再从模型 API 探测;显式配置同样跳过输出预留扣减(分母完全由操作者定义)(运行时热调:`/acp config`) |
242
+ | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`/acp` 命令展示窗口来源(模型工具 `acp_status` 不含窗口信息)。省略时窗口先读宿主投影(`windowFor` → `projectedContextWindow`,`src/window.ts`)再走探测;投影与探测在 `autoModelContextLimit: false` 时均跳过。探测失败会在宿主日志与 `/acp` 面板提示(`restart to re-probe`)——失败结果同样被缓存,修复网关后需重启或显式设置 `modelContextLimit` 才会重新探测。探测成功后还会**扣减 adapter 的每请求输出上限**(`defaultMaxTokens`,窗口末端每请求保证的输出预留):所有下游压力决策(nudge 档位、truncate、growth)以「可持续输入预算」(窗口 − 输出预留)为分母——96K 窗口 + 16K 上限实际最多承载 80K 输入,原裸窗口分母会把用量低估 cap/window(此处 ≈17%;小上下文窗口模型比例更高);上限未披露、显式配置或探测失败时保持裸窗口行为,`/acp status` 展示扣减明细(raw − reservation)。经 `/acp config` 改动 `modelContextLimit`/`autoModelContextLimit` 会清空窗口缓存,改完即重探 |
243
+ | `nudgeMinContextLimitPct` | 内核默认 `0.45` | Nudge 窗口下界(用量占比)——仅作配置校验,增长路径的触发没有百分比下限——与 billion-context-pi 相同的默认值(运行时热调:`/acp config`) |
244
+ | `nudgeMaxContextLimitPct` | engine 默认 `0.70`(内核/pi 默认 `0.75`) | 过限线:超过此值则无论增长与否都触发 nudge——刻意低于宿主 compaction-basic 的 80% 自动压缩线,保证强制 nudge 先触发;显式配置优先(`coreOverrides.nudge` 同名键优先级更高,见下)(运行时热调:`/acp config`) |
245
+ | `nudgeEmergencyThresholdPct` | engine 默认 `0.85`(内核/pi 默认 `0.95`) | 紧急 nudge(绕过每轮去重,但每个 user turn 最多注入 3 次——issue #108)——从 `0.95` 下调:95% 时模型已无操作空间且会被 80% 自动压缩线遮蔽;显式配置优先(`coreOverrides.nudge` 同名键优先级更高,见下)(运行时热调:`/acp config`) |
246
+ | `preset` | — | (可选)一句话选择 nudge 的激进程度:`preserve` / `relaxed` / `balanced` / `efficient` / `aggressive`(详见下文「预设」)。只填充你**未显式设置**的三个 nudge 阈值,优先级 显式值 > `preset` > engine 默认;未知名称在构造期报错,与显式阈值合并后若窗口反向(`min` / `max` / `emergency` 顺序错误)同样在构造期报错。不影响其他键(`modelContextLimit` / `autoNudge` / `prompts` / `coreOverrides`) (组合行专用:`preset` 尚未接入 `/acp config`,见 issue #75 后续) |
247
+ | `coreOverrides` | — | 任何其他 acp-kernel `Config` 覆盖(billion-context-pi 的 `coreOverrides` 逃生口)。合并顺序:内核默认 → 顶层 pct 配置 → `coreOverrides.nudge` 最后落地——同名键以它为准(只读:组合行专用,不经 settings 层) |
222
248
  | `autoTools` | `true` | 在 `ctx.tools` 注册四个模型工具 |
223
249
  | `autoCommand` | `true` | 在 `ctx.commands` 注册 `/acp` 命令 |
224
- | `autoNudge` | `true` | 当内核建议时向 `agent/pre-step` 注入 nudge |
250
+ | `autoNudge` | `true` | 当内核建议时向 `agent/pre-step` 注入 nudge(运行时热调:`/acp config`) |
251
+ | `settingsEnabled` | `true`(未配置即启用) | (可选)整体关闭运行时设置集成(组合行专用,不进 settings 层——开关不能关掉自己;关闭后组合行 `config:` 仍是唯一生效通道) |
225
252
  | `prompts` | — | (可选)自定义提示词文案:nudge / 范围表 / system prompt / 工具描述按槽位覆盖(模板 + 命名占位符,构造期校验;见上文「自定义提示词文案」与 [docs/configurable-prompts-design.md](docs/configurable-prompts-design.md)) |
226
253
 
254
+ ## 预设(preset)
255
+
256
+ 不想逐个调三个百分比时,用一个词选择 nudge 的激进程度——从「尽量保留上下文」到「尽早频繁压缩」共五档:
257
+
258
+ | `preset` | min | max | emergency | 定位 |
259
+ |---|---|---|---|---|
260
+ | `preserve` | 0.55 | 0.78 | 0.93 | 尽量保留上下文,接近上限才提醒压缩 |
261
+ | `relaxed` | 0.50 | 0.75 | 0.90 | 轻度压缩,比 preserve 稍早提醒 |
262
+ | `balanced` | 0.45 | 0.70 | 0.85 | 默认平衡——与插件开箱阈值一致(选它等于不改) |
263
+ | `efficient` | 0.40 | 0.60 | 0.78 | 更勤快地修剪,偏向低 token 占用而非保留完整历史 |
264
+ | `aggressive` | 0.30 | 0.50 | 0.70 | 精简上下文,更早更频繁地压缩 |
265
+
266
+ - **只填未设的阈值**:`preset` 仅填充你没有显式设置的 `nudge*ContextLimitPct`;同时写了 `preset` 和某个阈值时,该阈值以你的显式值为准(优先级 显式 > preset > 默认)。
267
+ - **不碰其他旋钮**:`modelContextLimit`、`autoNudge`、`prompts`、`coreOverrides` 完全不受影响;`coreOverrides.nudge` 仍最后落地、同名键最高优先。
268
+ - **查看当前档位**:`/acp status` 会打印生效的 `preset` 及其**真实生效的**三个阈值——这一行镜像 `kernelConfigFor` 的合并顺序,所以你在其上做的显式覆盖、以及 `coreOverrides.nudge` 里的同名键都会如实显示。
269
+ - **拼错即报错**:未知名称在引擎构造期直接抛错并列出合法值(与自定义提示词模板同一约定),不会静默回退默认。注意 bundle 行本身不带 `config`,`preset` 只能由你自己的同 id `compaction-acp` 行提供;该行构造失败即挂载失败,profile 会在你修好配置前一直起不来(fail-fast 的既定行为)。
270
+ - **运行时热切换**:预设目前走组合配置(安装 / `cordis.patch.yml`);待 #75 的 settings.yaml 热加载落地后,可在 `/acp config` 里改。本 PR 先让它在组合层可用。
271
+ - **反向窗口直接报错**:与显式阈值合并后若出现 `min > max`、`max > emergency` 或 `min > emergency`(例如 `preset: 'preserve'` 配 `nudgeMaxContextLimitPct: 0.5`),引擎在构造期抛错并列出三个值。内核对这种配置只打警告、不会拒绝,所以这道校验由引擎在 `resolveAcpConfig` 里补上。
272
+ - **与宿主 80% 线赛跑的是 `max`**:反复触发的过限提醒(`OVER-LIMIT`)由 `max` 决定;`preserve` / `relaxed` 的 emergency(0.93 / 0.90)高于宿主 compaction-basic 的 80% 线,只是超过它之后的标签升级,宿主先压缩时不会到达。
273
+ - **`min` 是首见提醒与 T2/T3 块数触发的地板**:内核 0.0.63 在运行时读它两处——① `firstSightMassReady`(从未提醒过、还没有基线、用量 ≥ `min` 且待压内容达到增长下限时立刻提醒一次,理由串带 `[first-sight mass]`);② `tierCountUsageFloor`(T2/T3 的「块数达标」触发同样要求用量 ≥ `min`)。常规 T1 增长提醒不看 `min`(由 `max` 与 `growthRatio` 决定),所以五档之间的主要差异仍来自 `max` 与 `emergency`,但更低的 `min` 会让首见提醒来得更早。
274
+ - **两个暂未纳入的旋钮**:原始需求里的 `growthRatio`(内核已有 `nudge.growthRatio`,可经 `coreOverrides` 调)和 `protectedLastMessages`(≈ 内核 `preserveRecentMessages`)目前不是本项目的一等旋钮;是否采纳为命名键 / UI 项由维护者决定,未擅自并入预设。
275
+
227
276
  ## 开发
228
277
 
229
278
  ```bash
@@ -231,8 +280,11 @@ npm install
231
280
  npm run typecheck # 严格 TS
232
281
  npm test # node --import tsx --test tests/*.test.ts
233
282
  npm run build # tsup 打包(内联 acp-kernel)+ .d.ts
283
+ npm run test:e2e # 端到端宿主回归:真实 agent 循环 + 脚本化假 LLM(见下文)
234
284
  ```
235
285
 
286
+ 端到端回归(`scripts/e2e/`)在进程内组装真实 DSH 宿主(cordis + agent-loop + DeepSeek 适配器),指向脚本化假 LLM 服务,挂载本引擎作为压缩后端,然后断言持久化事件日志:compaction 起止配对、durable replace 节点、严格 tool-call/result 配对、nudge 注入节奏;以及假 LLM **实际收到的请求体**(wire 级 prompt-cache 字节稳定性:envelope、`tools` 数组、leading message,无 compaction 场景全程 append-only)。背景与取舍见 [docs/e2e-harness-design.md](docs/e2e-harness-design.md)(issue #120)。
287
+
236
288
  `dist/index.js` 自包含,仅外链 `@deepseek-ai/*` 接缝包(由宿主部署提供)。
237
289
 
238
290
  ## 架构
@@ -243,6 +295,7 @@ src/
243
295
  ├── messages.ts # M1: 会话事件 ↔ acp-kernel CoreMessage 投影
244
296
  ├── state.ts # M2: 每会话内核状态
245
297
  ├── region.ts # M5: 持久化区域事务 + 日志重建块账本
298
+ ├── block-ledger.ts # M5 支持:tier/lineage 字段编码进 compaction/summary 的 rawOutput(绝不作顶层成员,issue #141)
246
299
  ├── tools.ts # M3: compress / decompress / search_context / acp_status
247
300
  ├── nudge.ts # M4: 内核压力决策 → 注入的建议式 nudge
248
301
  ├── system-prompt.ts# M4: 一次性 ACP 指引段(让 nudge 保持简短)
@@ -0,0 +1,66 @@
1
+ /**
2
+ * M5 support — durable block-ledger encoding inside `compaction/summary`.
3
+ *
4
+ * The frozen released-v0 reader (`@deepseek-ai/dsh-session-format-v0-to-v1`)
5
+ * validates every `compaction/summary` payload against an EXACT member
6
+ * allow-list and rejects the first member outside it (issue #141). The six ACP
7
+ * tier/lineage fields used to be written as top-level members, which bricked
8
+ * every pre-upgrade v0 session log the moment the host switched to that
9
+ * reader.
10
+ *
11
+ * The fix moves those fields OUT of the top-level members and INTO the already
12
+ * admitted optional `rawOutput` member, as one text content block carrying a
13
+ * namespaced JSON object. The frozen reader sees only admitted members; the
14
+ * namespaced payload survives losslessly and round-trips back through
15
+ * {@link decodeAcpBlockLedger}. `compaction/*` events are log-only, so this
16
+ * block is inert to the model and to surface derivation — pure durable storage.
17
+ *
18
+ * Decode is strict on the marker/version but lenient per-field, and it NEVER
19
+ * throws: an unknown/absent marker or a future version yields "no ledger data"
20
+ * (the caller falls back to tier-1 reconstruction). Old files and future format
21
+ * generations must degrade, not brick.
22
+ * @module billion-context-dsh/block-ledger
23
+ */
24
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm';
25
+ /** Marker key identifying an ACP block-ledger payload inside a `compaction/summary` rawOutput text block. */
26
+ export declare const ACP_BLOCK_LEDGER_MARKER = "$dshAcpBlockLedger";
27
+ /** Current block-ledger payload version. Bump (and add a legacy reader) when the shape changes. */
28
+ export declare const ACP_BLOCK_LEDGER_VERSION = 1;
29
+ /**
30
+ * The ACP tier/lineage fields carried durably per compressed block so a
31
+ * restarted engine rehydrates the SAME kernel blocks (tier, lineage, coverage)
32
+ * instead of collapsing everything to tier 1, plus B3's verified readings.
33
+ */
34
+ export interface AcpBlockLedgerPayload {
35
+ /** Compression tier: 1 (message range), 2 (distills tier-1), 3 (distills tier-2). */
36
+ readonly tier?: 1 | 2 | 3;
37
+ /** The acp-kernel block id (`bN`) created for this transaction. */
38
+ readonly kernelBlockId?: string;
39
+ /** Short block label (kernel `CompressionBlock.topic`). */
40
+ readonly topic?: string;
41
+ /** Durable compaction ids of the blocks distilled into this one. */
42
+ readonly parentBlockIds?: readonly string[];
43
+ /** The kernel block's direct message ids at creation (raw CoreMessage ids). */
44
+ readonly directMessageIds?: readonly string[];
45
+ /** The kernel block's effective message ids at creation (raw CoreMessage ids). */
46
+ readonly effectiveMessageIds?: readonly string[];
47
+ /** B3: acceptance readings already green before compression (e.g. "t0-fastpath 8/8"). */
48
+ readonly verifiedReadings?: readonly string[];
49
+ }
50
+ /**
51
+ * Encode the block-ledger fields as a single text content block whose text is a
52
+ * namespaced JSON object. The marker + version are always present so decode can
53
+ * recognise the payload; empty/absent fields are omitted to keep it minimal.
54
+ * The offline recovery normalizer reuses this exact encoder so rescued files
55
+ * match what a live engine writes (no shape drift).
56
+ */
57
+ export declare function encodeAcpBlockLedger(payload: AcpBlockLedgerPayload): ContentBlock[];
58
+ /**
59
+ * Decode + validate the block-ledger payload out of a `compaction/summary`
60
+ * `rawOutput` value read back from the log. Scans the content blocks for the
61
+ * namespaced JSON object, checks the marker/version strictly, accepts each field
62
+ * only if well-typed, and returns `{}` (no ledger data) on ANY problem — it
63
+ * never throws, because old files and future format generations must degrade to
64
+ * the tier-1 fallback rather than brick the session.
65
+ */
66
+ export declare function decodeAcpBlockLedger(rawOutput: unknown): AcpBlockLedgerPayload;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * M4 — the `/acp` slash command: a human-friendly window into the same
3
- * machinery the model tools expose (status, one-shot compress, decompress).
3
+ * machinery the model tools expose (status, one-shot compress, decompress,
4
+ * runtime settings read/write).
4
5
  * @module billion-context-dsh/commands
5
6
  */
6
7
  import type { CommandDefinition } from '@deepseek-ai/dsh-commands';
package/dist/config.d.ts CHANGED
@@ -21,7 +21,7 @@ export interface KernelConfigInput {
21
21
  readonly nudgeMinContextLimitPct?: number;
22
22
  /** Nudge window upper bound — over-limit guarantee line. Kernel default: 0.75. */
23
23
  readonly nudgeMaxContextLimitPct?: number;
24
- /** Emergency nudge threshold (bypasses per-turn dedup). Kernel default: 0.95. */
24
+ /** Emergency nudge threshold (bypasses per-turn dedup; capped at 3 injections per user turn — issue #108). Kernel default: 0.95. */
25
25
  readonly nudgeEmergencyThresholdPct?: number;
26
26
  /** Any other acp-kernel Config override (the billion-context-pi escape hatch). */
27
27
  readonly coreOverrides?: Partial<Config>;
@@ -11,10 +11,20 @@
11
11
  * display currency, NEVER event currency).
12
12
  *
13
13
  * This module prices claims in the host's vocabulary: it prefers the live
14
- * meter's own per-node prices (`ctx.tokenMeter.measure(session).nodes` —
15
- * exact by construction, follows host estimator changes automatically, the
16
- * same path the host's own `compaction-basic` uses) and falls back to an
14
+ * meter's own per-node FIXED-HEURISTIC prices (`ctx.tokenMeter.measure(session)`
15
+ * nodes' `heuristicTokens` — the same basis the projection ledger accumulates
16
+ * appends with, so the claim is exact by construction) and falls back to an
17
17
  * exact mirror of the host's estimator when the meter is unreachable.
18
+ *
19
+ * Two vocabularies share the meter's node since DSH 0.1.2: `tokens` carries
20
+ * the measured route's request pressure (image occurrences re-priced with the
21
+ * route's declared visual tokens) while `heuristicTokens` keeps the fixed
22
+ * flat-4 heuristic the ledger prices appends with. The claim MUST read
23
+ * `heuristicTokens`: a routed `tokens` claim overstates the replaced range
24
+ * against its own ledger accumulation and folds `messageTokens` negative —
25
+ * the same session-bricking schema rejection as #54, through the image-route
26
+ * channel (issue #103). Older hosts (0.1.0/0.1.1 lines) expose a single
27
+ * `tokens` field that IS the fixed heuristic, so the fallback reads it.
18
28
  */
19
29
  import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
20
30
  /** The host's model-visible content block union (structural, mirror-side only). */
@@ -59,10 +69,68 @@ export declare function estimateHostMessage(message: {
59
69
  export declare function hostPriceEvent(event: SessionEvent): number;
60
70
  /** Mirror price of a set of surface seqs (the fallback claim computation). */
61
71
  export declare function shadowedHostTokens(session: Session, seqs: readonly number[]): number;
72
+ /**
73
+ * Fixed-heuristic price of the media blocks reachable in `blocks` (any nesting
74
+ * depth, tool results included) — an exact mirror of the host's
75
+ * `estimateStructuralBlock`
76
+ * (`@deepseek-ai/dsh-token-meter/lib/types/estimate.js`), the arm the host's own
77
+ * pricing takes for an image/file reference, "whose request price is route-owned
78
+ * rather than fixed".
79
+ *
80
+ * Used as the media price whenever the meter reports no routed surcharge for the
81
+ * seq — which is every host today — so a media-bearing span is never priced as
82
+ * if the picture were free. It is a DISPLAY price only; it must never reach a
83
+ * `shadowedTokenCount` claim (rule 12).
84
+ *
85
+ * UPSTREAM: drop this mirror the moment dsh-token-meter exports
86
+ * `estimateStructuralBlock` (same tracker entry as `estimateContent`, see
87
+ * docs/dsh-porting-verification.md).
88
+ */
89
+ export declare function hostMediaStructuralPrice(blocks: unknown): number;
90
+ /**
91
+ * Provider-anchored MEDIA price per surface seq, read from the host meter.
92
+ *
93
+ * An `image`/`file` block carries no characters, so every text-based estimator
94
+ * prices it at zero while the provider still bills it. The only media signal the
95
+ * meter's public node exposes is the gap between its two prices: `tokens` is the
96
+ * route-priced request pressure — a media occurrence carries the adapter's
97
+ * declared visual price when there is one, which is why the host itself reads
98
+ * this field for range selection — and `heuristicTokens` is the route-independent
99
+ * fixed heuristic. Their difference is exactly the routed surcharge the host
100
+ * already charged the route.
101
+ *
102
+ * That difference is ZERO on every adapter that declares no visual price (all
103
+ * production adapters today, and the pinned test meter), so callers ADD the
104
+ * fixed-heuristic `hostMediaStructuralPrice` on top instead of reading an absent
105
+ * surcharge as a free image. Without this pair a picture-heavy span looked free
106
+ * in the compressible-range table and the model ranked it last (issue #117).
107
+ *
108
+ * Where this price may be used: USAGE accounting only (range-table tokens,
109
+ * display). It must NEVER feed a `shadowedTokenCount` claim — the host's
110
+ * projection folds those with its own fixed heuristic and a routed price
111
+ * overstates the claim, folding the projection negative on image ranges
112
+ * (issue #103, the image-route channel of the #54 brick; rule 12).
113
+ *
114
+ * Returns an empty map when the meter is absent, when a measurement throws
115
+ * (step-less logs), or when the meter exposes no media fields (older line) —
116
+ * which degrades to the previous text-only behaviour.
117
+ */
118
+ export declare function mediaPriceViaMeter(session: Session, ctx?: {
119
+ get?(name: string): unknown;
120
+ } | null): ReadonlyMap<number, number>;
62
121
  /**
63
122
  * Claim price for `seqs` in the host's vocabulary. Prefers the live meter's
64
- * own per-node prices when `ctx.tokenMeter` is reachable and covers every
65
- * shadowed seq (exact by construction, follows host estimator changes); ANY
123
+ * own per-node FIXED-HEURISTIC prices when `ctx.tokenMeter` is reachable and
124
+ * covers every shadowed seq (exact by construction — the ledger's
125
+ * `foldSurfaceProjection` accumulates appends with the same fixed heuristic,
126
+ * so the claim and the ledger stay in agreement; follows host estimator
127
+ * changes automatically). `node.heuristicTokens` is that basis since DSH 0.1.2;
128
+ * `node.tokens` there is the measured route's REQUEST pressure (image
129
+ * occurrences carry the route's visual price via `priceSurface`) and MUST NOT
130
+ * be claimed — reading it overstates the claim and folds the host projection
131
+ * negative on image-containing ranges (issue #103, the image-route channel of
132
+ * the #54 brick). Older meters expose a single `tokens` field that IS the
133
+ * fixed heuristic, so `heuristicTokens ?? tokens` covers both shapes. ANY
66
134
  * failure — meter absent, `measure` throwing (e.g. a step-less log), or a seq
67
135
  * missing from the measurement — falls back to the exact mirror. Never returns
68
136
  * a `defaultCountTokens` price (rule 12).