diff --git a/announcements/remote.json b/announcements/remote.json
index a533cf87f5..3234e5b73c 100644
--- a/announcements/remote.json
+++ b/announcements/remote.json
@@ -1,4 +1,27 @@
{
"v": 1,
- "entries": []
+ "entries": [
+ {
+ "id": "dsh-live-compact-conflict",
+ "kind": "warning",
+ "tone": "warn",
+ "title": {
+ "en": "DSH: dsh-live-compact conflicts with bili compression",
+ "zh-CN": "DSH:dsh-live-compact 与 bili 压缩冲突"
+ },
+ "desc": {
+ "en": "If your dsh profile has the third-party plugin dsh-live-compact installed, it acts as a competing compaction engine that bili's compaction-disabling overlay cannot see — its checkpoints can land outside bili's knowledge and destroy the compression substrate (#5984). The conflict scan now flags it as a known conflict. If bili should own compression, remove the plugin and restart the profile: dsh plugin remove dsh-live-compact",
+ "zh-CN": "如果你的 dsh profile 安装了第三方插件 dsh-live-compact,它是一个 bili 压缩禁用 overlay 看不见的竞争压缩引擎——它的压缩 checkpoint 可能落在 bili 知识之外、摧毁压缩基底(#5984)。冲突扫描现在会把它标记为已知冲突。若要让 bili 持有压缩,请卸载插件并重启 profile:dsh plugin remove dsh-live-compact"
+ },
+ "hint": {
+ "en": "Source-level analysis in issue #5984: the plugin takes over dsh's stock compaction engine, and its row name never matches bili's overlay.",
+ "zh-CN": "源码级分析见 issue #5984:该插件顶替 dsh 的 stock 压缩引擎,其行名永远对不上 bili 的 overlay。"
+ },
+ "ctaLabel": "See #5984",
+ "ctaUrl": "https://github.com/ranxianglei/billion-context/issues/5984",
+ "dismiss": "persistent",
+ "priority": 70,
+ "publishedAt": "2026-11-11T10:40:00Z"
+ }
+ ]
}
diff --git a/docs/configuration.md b/docs/configuration.md
index 3e89d041a4..77e5617a3d 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -2146,7 +2146,7 @@ File keys resolve only when the matching env var is unset. Defaults in parenthes
| `BILI_CHAIN_STAMP` | Set `1` to turn ON egress emission of the **model-visible** `` chain-integrity checkpoint carrier (#1683, default OFF): when enabled, every request THIS instance actually processes leaves with a digest-bearing stamp so a downstream bili applies first-processor-wins even when the `x-bili-hop` header was stripped in transit (#1421). The carrier lands in a slot the terminal model also reads (a trailing `user` message on OpenAI/Responses, a trailing text part on Anthropic/Google), so models treat it as phantom user input and burn tokens commenting on it — which is why it is off by default. Enable it only for the narrow multi-bili relay case where `x-bili-hop` is stripped by an intermediate box and the digest-verified body-stamp is the sole guard against double-processing. Independent of `BILI_CHAIN_CONTENT` (inbound body-detection is also default OFF) and of the `x-bili-hop` passthrough, which works either way. `"chainEgressStamp": true` in the config file does the same; the env var wins. |
| `BILI_CHAIN_MAX_FUTURE_SKEW_MS` | Maximum future skew (ms) tolerated when validating a chain checkpoint's `issued-at` timestamp (#1395 step 2): a checkpoint stamped more than this far into the future is judged `stale` (replay / clock skew) even when its digest verifies. Default `120000` (2 min); non-numeric or non-positive values fall back to the default. Step 2 is shadow-only — these knobs tune verdict logging, never forwarding. |
| `BILI_CHAIN_RECENT_WINDOW_MS` | Recency window (ms) for chain-checkpoint validation (#1395 step 2): a checkpoint older than this is judged `stale`. Default `600000` (10 min); non-numeric or non-positive values fall back to the default. Step 2 is shadow-only — these knobs tune verdict logging, never forwarding. |
-| `BILI_CONFLICT_SCAN` | Set `0` to disable third-party compression plugin detection (#1206). On by default: bili scans the client's own plugin/extension registries — opencode global + project `plugin` arrays, pi global + project `.pi/settings.json` `packages`, omp `config.yml` `extensions`, claude settings `enabledPlugins`/`plugins` + its plugins dir, kimi `plugins/installed.json`, hermes plugins dir, dsh profile dependencies — for another compressor co-resident with bili. Two tiers: **known conflicts** (`opencode-acp`, legacy `billion-context-pi`) and **keyword-suspected** entries (names matching compress / compact / acp / summar* / context*; bili's own entries are always skipped, and non-compression tools like `context7` do not match). Findings surface as launcher stderr lines before client start, a one-time proxy warn log on each session's first request, and the session's conflict ledger — visible in `acp_status`'s `COMPRESSION CONFLICTS` section, aggregated at `GET /__bili/stats` → `conflicts`, and shown as a web-UI banner. The banner is an alarm for interception **failure** (#3235): it pops only when landed-unrecovered evidence exists (a confirmed third-party compressor, an inferred client-native landing, or an unexplained history-rewrite signal) — recovered (intercepted+rebased) landings and suspected/sibling stock stay recorded but quiet. It offers a persistent don't-show-again (localStorage watermark that re-arms automatically on any newer event) and copy-diagnostics / report-on-GitHub buttons riding the generic #3040 diagnostics facility. Runtime interference evidence (unannounced history rewrites #1001, orphan-block deactivations) feeds the same ledger. The scan is read-only, best-effort, 5-minute cached, and never blocks or modifies client config. |
+| `BILI_CONFLICT_SCAN` | Set `0` to disable third-party compression plugin detection (#1206). On by default: bili scans the client's own plugin/extension registries — opencode global + project `plugin` arrays, pi global + project `.pi/settings.json` `packages`, omp `config.yml` `extensions`, claude settings `enabledPlugins`/`plugins` + its plugins dir, kimi `plugins/installed.json`, hermes plugins dir, dsh profile dependencies — for another compressor co-resident with bili. Two tiers: **known conflicts** (`opencode-acp`, legacy `billion-context-pi`, and dsh's `dsh-live-compact`, #5984) and **keyword-suspected** entries (names matching compress / compact / acp / summar* / context*; bili's own entries are always skipped, and non-compression tools like `context7` do not match). Findings surface as launcher stderr lines before client start, a one-time proxy warn log on each session's first request, and the session's conflict ledger — visible in `acp_status`'s `COMPRESSION CONFLICTS` section, aggregated at `GET /__bili/stats` → `conflicts`, and shown as a web-UI banner. The banner is an alarm for interception **failure** (#3235): it pops only when landed-unrecovered evidence exists (a confirmed third-party compressor, an inferred client-native landing, or an unexplained history-rewrite signal) — recovered (intercepted+rebased) landings and suspected/sibling stock stay recorded but quiet. It offers a persistent don't-show-again (localStorage watermark that re-arms automatically on any newer event) and copy-diagnostics / report-on-GitHub buttons riding the generic #3040 diagnostics facility. Runtime interference evidence (unannounced history rewrites #1001, orphan-block deactivations) feeds the same ledger. The scan is read-only, best-effort, 5-minute cached, and never blocks or modifies client config. |
| `BILI_UPSTREAM_PROXY` | Upstream proxy for the proxy's own outbound connections — highest priority among the *global* sources (above the Web UI manual proxy and the config-file `proxy`). A per-URL `providers..proxy` still wins for its matching provider URL. See [`proxy`](#server-settings) under Server Settings for the full resolution order, loop prevention, and examples. |
| `BILI_INHERITED_HTTP_PROXY` / `BILI_INHERITED_HTTPS_PROXY` / `BILI_INHERITED_ALL_PROXY` / `BILI_INHERITED_NO_PROXY` | Not user-facing — set automatically by the launcher when it spawns the proxy (#1012). The launcher strips the shell's proxy vars from both the client and the proxy child (clients must send to bili; the proxy must not have its model egress hijacked by a shell proxy), but it forwards the user's pre-strip proxy under these names so the proxy's **auxiliary egress** (MITM blind tunnels — client-side MCP/web traffic) can still ride the user's VPN. They feed only the blind-tunnel fallback tier: explicit routes / global `proxy` / `BILI_UPSTREAM_PROXY` / explicit `"upstreamProxyMode": "direct"` all still win, and a value pointing at bili's own port is dropped. Model egress is unaffected (stays direct unless explicitly configured). |
| `BILI_ATTACH_HEALTH_DEADLINE_MS` | Health-wait deadline (ms) for dsh/opencode attach verification when the attach target is down but this process's model channel is **pinned** to it (routed `/bili/…` model traffic was observed against it): bili waits for the target to come back instead of spawning a second instance — a spawn would split the session (model traffic stays pinned, bili tools would 404 against the other instance). When the deadline elapses it fails loudly and keeps re-checking on every model request until the target returns (default `15000`). See #1365. |
@@ -2519,7 +2519,7 @@ Kill switch: `BILLION_CONTEXT_PLUGIN=0` disables plugin mode entirely (wire-leve
Two compressors on one conversation double-compress and corrupt message refs, so bili actively looks for other compressors co-resident with itself:
-- **Scan** (read-only, best-effort, 5-minute cache): opencode global + project config `plugin` arrays; pi global + project `.pi/settings.json` `packages`; omp `config.yml` `extensions`; claude settings `enabledPlugins`/`plugins` keys + `~/.claude/plugins/` dir; kimi `plugins/installed.json`; hermes `~/.hermes/plugins/` dir; dsh profile `package.json` dependencies. Two tiers: **known conflicts** (`opencode-acp`, legacy `billion-context-pi` — deterministic) and **keyword-suspected** entries (names matching compress* / compact* / acp / summar*; bare `context` deliberately does NOT match — it names the domain, not the act of compressing, so read-only tools like `context7`/`dsh-context` do not flag); verified non-compressing plugins are exempt outright by EXACT package name — read-only/display-only tools (e.g. `pi-compact-transcript`, `pi-context-inspector`) and protocol adapters whose name merely collides with a keyword (`@openma/deepseek-harness-acp`: its `acp` means Agent Client Protocol, not the acp-kernel ecosystem marker; #2969) — lookalike names still flag.
+- **Scan** (read-only, best-effort, 5-minute cache): opencode global + project config `plugin` arrays; pi global + project `.pi/settings.json` `packages`; omp `config.yml` `extensions`; claude settings `enabledPlugins`/`plugins` keys + `~/.claude/plugins/` dir; kimi `plugins/installed.json`; hermes `~/.hermes/plugins/` dir; dsh profile `package.json` dependencies. Two tiers: **known conflicts** (`opencode-acp`, legacy `billion-context-pi`, and dsh's `dsh-live-compact` (#5984) — deterministic) and **keyword-suspected** entries (names matching compress* / compact* / acp / summar*; bare `context` deliberately does NOT match — it names the domain, not the act of compressing, so read-only tools like `context7`/`dsh-context` do not flag); verified non-compressing plugins are exempt outright by EXACT package name — read-only/display-only tools (e.g. `pi-compact-transcript`, `pi-context-inspector`) and protocol adapters whose name merely collides with a keyword (`@openma/deepseek-harness-acp`: its `acp` means Agent Client Protocol, not the acp-kernel ecosystem marker; #2969) — lookalike names still flag. dsh's `dsh-live-compact` is a known conflict, not a keyword suspicion: it is a competing compaction engine — its bundled `cordis.patch.yml` disables the stock `compaction-basic` row and registers its own engine row, and its preset takeover renames the preset's compaction row to itself, so bili's load-time compaction-disable overlay (#3354), which recognizes only the stock row names, is structurally blind to it and its checkpoints land outside bili's knowledge (#5984).
- **Where findings surface:** launcher stderr before the client starts; a proxy warn log on each session's first request (client identified from the `x-bili-plugin` header or wire headers); and the session's conflict ledger — `acp_status`'s `COMPRESSION CONFLICTS` section, `GET /__bili/stats` → `conflicts`, web-UI banner.
- **Runtime evidence, tiered for display (#2545):** unannounced history rewrites (#1001) and orphan-gc deactivations (summarized content deleted out of the client's history) are recorded in the same ledger as *suspected* coexistence. Severity surfaces grade by EVIDENCE TIER: **confirmed** = a detected client-native compaction landing (bili rebases onto it, #2372/#2432) or a non-suspected plugin finding — only this triggers the imperative "keep one compressor" warning; **unconfirmed signals** = unannounced rewrites + orphan-gc deactivations — observed changes whose cause is NOT identified ("history changed, cause unknown"), never presented as native auto-compaction or a second compressor; name-only [suspected] matches stay in the soft "verify first" tier (#1736) and a PURE [suspected] ledger gets verify-first advice only — no imperative per-client fix footer ("disable auto-compaction / start a fresh session") while nothing is confirmed (#2969). Stock ledger records naming verified non-compressing plugins (read-only viewers, or protocol adapters such as #2969's ACP adapter) are neutralized at display time instead of persisting as alarms, and cross-session aggregate counts of unconfirmed signals never escalate to a same-conversation double-compression claim.
- **dsh's `auto: false` disables only the automatic triggers.** The `compaction-basic: { auto: false }` line installed by the profile bundle patch (`dsh.bundle.patch.yml`) skips pressure/overflow auto-compaction — a manual `/compact` (and idle-session compaction) still fires. Calls routed through bili are refused by the server-side guard (#1729/#2360); calls that reach the provider without transiting bili (desktop-lane paths the plugin's takeover gate does not attribute) land, and bili detects them on the next replay — checkpoint framing + decimated fold coverage — rebasing its compression state in one turn instead of failing every later compress forever (#2432). When the goal is turning dsh compaction off **entirely** — including manual `/compact` — the delta belongs on that same row as `disabled: true`, not `auto: false` (recipe below).
diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md
index c5f18d030e..2d61e133a9 100644
--- a/docs/configuration.zh-CN.md
+++ b/docs/configuration.zh-CN.md
@@ -2118,7 +2118,7 @@ ACP 原生 agent(当前为 `pi` 扩展)会在每个进程内向代理上报
| `BILI_CHAIN_STAMP` | 设为 `1` 开启**模型可见**的 `` 链完整性检查点载体的出站注入(#1683,默认关闭):开启后,本实例实际处理的每个请求都携带一个带摘要的戳,使下游 bili 即使 `x-bili-hop` 头在传输中被剥离也能应用「首个处理器优先」(first-processor-wins)(#1421)。该载体落在终端模型同样会读取的位置(OpenAI/Responses 上是一条尾部 `user` 消息,Anthropic/Google 上是尾部文本 part),因此模型会把它当作幽灵用户输入并花 token 去评论它——这正是它默认关闭的原因。仅在多 bili 中继、且中间盒子剥掉 `x-bili-hop`、带摘要校验的 body-stamp 是防止双重处理的唯一手段这一狭窄场景下才启用。与 `BILI_CHAIN_CONTENT`(入站正文检测同样默认关闭)及 `x-bili-hop` 透传相互独立(后者无论如何都生效)。配置文件中设 `"chainEgressStamp": true` 效果相同;环境变量优先。 |
| `BILI_CHAIN_MAX_FUTURE_SKEW_MS` | 校验链检查点 `issued-at` 时间戳时容忍的最大未来偏斜(毫秒)(#1395 step 2):戳在比当前时间未来超过此值的检查点会被判为 `stale`(重放 / 时钟偏斜),即使其摘要校验通过。默认 `120000`(2 分钟);非数字或非正值回退到默认值。Step 2 仅影子模式——这些旋钮只调判定日志,绝不影响转发。 |
| `BILI_CHAIN_RECENT_WINDOW_MS` | 链检查点校验的近期窗口(毫秒)(#1395 step 2):早于此窗口的检查点被判为 `stale`。默认 `600000`(10 分钟);非数字或非正值回退到默认值。Step 2 仅影子模式——这些旋钮只调判定日志,绝不影响转发。 |
-| `BILI_CONFLICT_SCAN` | 设为 `0` 关闭第三方压缩插件检测(#1206)。默认开启:bili 会扫描客户端自身的插件/扩展注册表 —— opencode 全局 + 项目配置的 `plugin` 数组、pi 全局 + 项目 `.pi/settings.json` 的 `packages`、omp `config.yml` 的 `extensions`、claude 设置的 `enabledPlugins`/`plugins` 及其插件目录、kimi `plugins/installed.json`、hermes 插件目录、dsh profile 依赖 —— 查找与 bili 并存的另一个压缩器。两个层级:**已知冲突**(`opencode-acp`、遗留 `billion-context-pi`,确定性判定)和**关键词疑似**条目(名称匹配 compress / compact / acp / summar* / context*;bili 自身条目永远跳过,`context7` 这类非压缩工具不会误报)。发现结果出现在:客户端启动前的 launcher stderr、每个会话首个请求的一次性代理 warn 日志、以及会话冲突台账 —— `acp_status` 的 `COMPRESSION CONFLICTS` 段、`GET /__bili/stats` → `conflicts`、Web UI 横幅。横幅是拦截**失败**的告警(#3235):只有存在已落地且未恢复的证据(已确认的第三方压缩器、推断出的客户端原生落点、或无法解释的历史改写信号)才弹出——已恢复(拦截+rebase)的落点与疑似/同族存量保持记录但静默;提供持久化「不再提示」(localStorage 水位线,任何更新的事件自动重新武装)与复制诊断/去 GitHub 报告按钮(复用通用 #3040 诊断设施)。运行时干扰证据(未宣告的历史改写 #1001、孤儿块废弃)记入同一台账。「扫描只读、尽力而为、5 分钟缓存,绝不阻塞或改动客户端配置。」 |
+| `BILI_CONFLICT_SCAN` | 设为 `0` 关闭第三方压缩插件检测(#1206)。默认开启:bili 会扫描客户端自身的插件/扩展注册表 —— opencode 全局 + 项目配置的 `plugin` 数组、pi 全局 + 项目 `.pi/settings.json` 的 `packages`、omp `config.yml` 的 `extensions`、claude 设置的 `enabledPlugins`/`plugins` 及其插件目录、kimi `plugins/installed.json`、hermes 插件目录、dsh profile 依赖 —— 查找与 bili 并存的另一个压缩器。两个层级:**已知冲突**(`opencode-acp`、遗留 `billion-context-pi`、dsh 的 `dsh-live-compact`(#5984),确定性判定)和**关键词疑似**条目(名称匹配 compress / compact / acp / summar* / context*;bili 自身条目永远跳过,`context7` 这类非压缩工具不会误报)。发现结果出现在:客户端启动前的 launcher stderr、每个会话首个请求的一次性代理 warn 日志、以及会话冲突台账 —— `acp_status` 的 `COMPRESSION CONFLICTS` 段、`GET /__bili/stats` → `conflicts`、Web UI 横幅。横幅是拦截**失败**的告警(#3235):只有存在已落地且未恢复的证据(已确认的第三方压缩器、推断出的客户端原生落点、或无法解释的历史改写信号)才弹出——已恢复(拦截+rebase)的落点与疑似/同族存量保持记录但静默;提供持久化「不再提示」(localStorage 水位线,任何更新的事件自动重新武装)与复制诊断/去 GitHub 报告按钮(复用通用 #3040 诊断设施)。运行时干扰证据(未宣告的历史改写 #1001、孤儿块废弃)记入同一台账。「扫描只读、尽力而为、5 分钟缓存,绝不阻塞或改动客户端配置。」 |
| `BILI_UPSTREAM_PROXY` | 代理自身出站连接的上游代理 —— 在*全局*来源中优先级最高(高于 Web UI 手动代理与配置文件 `proxy`)。per-URL 的 `providers..proxy` 对其匹配的 provider URL 仍然优先。完整解析顺序、防环规则与示例见 [服务端设置 → `proxy`](#服务端设置)。 |
| `BILI_INHERITED_HTTP_PROXY` / `BILI_INHERITED_HTTPS_PROXY` / `BILI_INHERITED_ALL_PROXY` / `BILI_INHERITED_NO_PROXY` | 非用户直接使用 —— launcher 起代理子进程时自动设置(#1012)。launcher 会从客户端和代理子进程两侧剥掉 shell 的代理变量(客户端必须把流量发给 bili;代理的模型出网也不能被 shell 代理劫持),但会把用户剥离前的代理转发到这些变量里,让代理的**辅助出网**(MITM 盲隧道 —— 客户端侧的 MCP/web 流量)仍能走用户的 VPN。它们只作用于盲隧道的 fallback 层:显式路由 / 全局 `proxy` / `BILI_UPSTREAM_PROXY` / 显式 `"upstreamProxyMode": "direct"` 仍然优先,指向 bili 自身端口的值会被丢弃。模型出网不受影响(未显式配置则保持直连)。 |
| `BILI_UPSTREAM_TIMEOUT_MS` | 上游请求的空闲预算(毫秒):首字节时间(TTFB)与响应体块之间的间隔(默认 `720000` = 12 分钟)。持续产出数据块的健康流永远不会被中途切断;静默的流才会。同一个值同时驱动底层 HTTP 客户端的传输层超时,因此这一个旋钮即可端到端约束本地大模型的超长 prefill(#551)。 |
@@ -2482,7 +2482,7 @@ bili plugin remove pi # 撤销(原文件一次性备份为 *.bili-bak)
两个压缩器作用于同一会话会双压缩、破坏消息引用,所以 bili 会主动查找与自己并存的另一个压缩器:
-- **扫描**(只读、尽力而为、5 分钟缓存):opencode 全局 + 项目配置的 `plugin` 数组;pi 全局 + 项目 `.pi/settings.json` 的 `packages`;omp `config.yml` 的 `extensions`;claude 设置的 `enabledPlugins`/`plugins` 键 + `~/.claude/plugins/` 目录;kimi `plugins/installed.json`;hermes `~/.hermes/plugins/` 目录;dsh profile 的 `package.json` 依赖。两个层级:**已知冲突**(`opencode-acp`、遗留 `billion-context-pi`,确定性判定)和**关键词疑似**条目(名称匹配 compress* / compact* / acp / summar*;裸 `context` 刻意不匹配——它指领域而非压缩动作本身,`context7`/`dsh-context` 这类只读工具不会误报);已验证的只读/仅显示插件(如 `pi-compact-transcript`、`pi-context-inspector`)直接豁免。
+- **扫描**(只读、尽力而为、5 分钟缓存):opencode 全局 + 项目配置的 `plugin` 数组;pi 全局 + 项目 `.pi/settings.json` 的 `packages`;omp `config.yml` 的 `extensions`;claude 设置的 `enabledPlugins`/`plugins` 键 + `~/.claude/plugins/` 目录;kimi `plugins/installed.json`;hermes `~/.hermes/plugins/` 目录;dsh profile 的 `package.json` 依赖。两个层级:**已知冲突**(`opencode-acp`、遗留 `billion-context-pi`、dsh 的 `dsh-live-compact`(#5984),确定性判定)和**关键词疑似**条目(名称匹配 compress* / compact* / acp / summar*;裸 `context` 刻意不匹配——它指领域而非压缩动作本身,`context7`/`dsh-context` 这类只读工具不会误报);已验证的只读/仅显示插件(如 `pi-compact-transcript`、`pi-context-inspector`)直接豁免。dsh 的 `dsh-live-compact` 列为已知冲突而非关键词疑似:它是竞争压缩引擎——随包 `cordis.patch.yml` 禁用 stock `compaction-basic` 行、注册自己的引擎行(`compaction-spine`/`dsh-live-compact`)、preset 接管时把 preset 的压缩行改名成自己,而 bili 的加载期压缩禁用 overlay(#3354)只认 stock 行名,对它在结构上不可见,其压缩 checkpoint 会落在 bili 知识之外(#5984)。
- **发现结果的出口**:客户端启动前的 launcher stderr;每个会话首个请求的一次性代理 warn 日志(client 由 `x-bili-plugin` 头或 wire 头识别);会话冲突台账 —— `acp_status` 的 `COMPRESSION CONFLICTS` 段、`GET /__bili/stats` → `conflicts`、Web UI 横幅。
- **运行时证据,按证据层级展示(#2545)**:未宣告的历史改写(#1001)与孤儿块废弃(被摘要的内容从客户端历史中被删掉)与「疑似并存」记入同一台账。严重程度表面按**证据层级**分级:**已确认** = 检测到的客户端原生压缩落点(bili 会 rebase 到其上,#2372/#2432)或非 suspected 的插件发现——只有它会触发「请只保留一个压缩器」的强制告警;**未确认信号** = 未宣告改写 + 孤儿块废弃——观测到变化但原因未确认(「历史发生了变化,原因未知」),绝不呈现为原生自动压缩或第二个压缩器;纯名称匹配的 [suspected] 停留在软性的「先核实」层级(#1736)。指向已验证只读插件的存量台账记录在显示期被中和,而不是持续作为告警残留;跨会话聚合的未确认信号计数永远不会升级成「同一会话双压缩」的结论。
- **dsh 的 `auto: false` 只关闭自动触发**。profile bundle patch(`dsh.bundle.patch.yml`)写入的 `compaction-basic: { auto: false }` 跳过压力/溢出自压缩 —— 手动 `/compact`(以及空闲会话压缩)仍会触发。经 bili 路由的调用会被服务端闸门拒绝(#1729/#2360);未经过 bili 直达上游的调用(桌面端插件接管门无法归因的路径)会落地,bili 在下次重放时检测出来(checkpoint 框架 + 折叠覆盖缺口),一个 turn 内重建自己的压缩状态,而不是让之后每次 compress 永久失败(#2432)。若目标是把 dsh 压缩**整个关掉**(含手动 `/compact`),delta 应写成同一行上的 `disabled: true`,而不是 `auto: false`(配方见下)。
diff --git a/src/thirdparty-scan.ts b/src/thirdparty-scan.ts
index 6fc46fce44..09d62a83da 100644
--- a/src/thirdparty-scan.ts
+++ b/src/thirdparty-scan.ts
@@ -7,7 +7,8 @@
// always install OTHER third-party context-compression plugins alongside
// bili — nothing detects that today. This module scans the client's plugin
// registry (best-effort, read-only, cached) for:
-// 1. known conflicting entries (bili's siblings, when not absorbed by design);
+// 1. known conflicting entries (bili's siblings, when not absorbed by design,
+// plus dsh's dsh-live-compact takeover compaction engine, #5984);
// 2. suspected compression plugins by name keyword (compress*/compact*/acp/
// summar*) — flagged for user confirmation, never auto-fixed.
// Callers: launcher pre-launch warning, proxy first-request-per-session
@@ -192,6 +193,17 @@ export function isOpencodeAcpEntry(entry: string): boolean {
return bare === "opencode-acp" || /^opencode-acp@/.test(bare) || /(^|[/\\])opencode-acp([/\\]|$)/.test(trimmed);
}
+/** #5984: dsh-live-compact entry identity — bare name, npm spec (with or
+ * without a version), or any path whose segments contain dsh-live-compact.
+ * Bounded segment match mirroring isOpencodeAcpEntry's discipline so
+ * lookalikes ("dsh-live-compact-viewer") stay out of the known tier and at
+ * most keyword-suspected. */
+export function isDshLiveCompactEntry(entry: string): boolean {
+ const trimmed = entry.trim();
+ const bare = trimmed.replace(/^npm:/, "");
+ return bare === "dsh-live-compact" || /^dsh-live-compact@/.test(bare) || /(^|[/\\])dsh-live-compact([/\\]|$)/.test(trimmed);
+}
+
// #2324/#2545/#2969: third-party plugins VERIFIED to never compress the model
// context — so their names must not raise a compression-conflict alarm. Two
// evidence classes: read-only display/viewer plugins, and protocol adapters
@@ -430,6 +442,19 @@ function scanDsh(env: NodeJS.ProcessEnv): ScanResult {
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
for (const dep of Object.keys(deps as Record)) {
if (dep === "billion-context") continue;
+ if (isDshLiveCompactEntry(dep)) {
+ // #5984: a competing compaction engine, not a keyword
+ // suspicion. Its bundled cordis.patch.yml disables the
+ // stock compaction-basic row and registers its own engine
+ // row (compaction-spine / dsh-live-compact), and its preset
+ // takeover renames the preset's compaction row to itself —
+ // bili's load-time compaction-disable overlay (#3354)
+ // recognizes only the stock row names, so this plugin is
+ // structurally invisible to it and its checkpoints land
+ // outside bili's knowledge.
+ add(c, { client: "dsh", entry: dep, source: file, match: "known", knownId: "dsh-live-compact" });
+ continue;
+ }
if (isKnownDisplayOnlyPlugin(dep)) continue;
if (KEYWORD_RE.test(dep)) add(c, { client: "dsh", entry: dep, source: file, match: "keyword" });
}
diff --git a/tests/issue1206-thirdparty-scan.test.ts b/tests/issue1206-thirdparty-scan.test.ts
index 92007254ef..8fc99c825a 100644
--- a/tests/issue1206-thirdparty-scan.test.ts
+++ b/tests/issue1206-thirdparty-scan.test.ts
@@ -8,7 +8,7 @@ process.env.NODE_ENV = "test";
import { createInitialState } from "acp-kernel";
import type { Session } from "../src/session.ts";
-import { clearScanCache, conflictScanEnabled, isDesignBenign, isDisplayOnlyConflictDetail, isKnownDisplayOnlyPlugin, isOpencodeAcpEntry, isSiblingConflictDetail, parsePluginConflictDetail, scanClientPlugins, sniffScanClient, type ThirdPartyFinding } from "../src/thirdparty-scan.js";
+import { clearScanCache, conflictScanEnabled, isDesignBenign, isDisplayOnlyConflictDetail, isKnownDisplayOnlyPlugin, isDshLiveCompactEntry, isOpencodeAcpEntry, isSiblingConflictDetail, parsePluginConflictDetail, scanClientPlugins, sniffScanClient, type ThirdPartyFinding } from "../src/thirdparty-scan.js";
import { CONFLICT_LEDGER_MAX, conflictEventsOf, formatConflictSection, recordConflict, summarizeConflicts } from "../src/conflict-watch.js";
import { resolveHermesHome, resolveKimiHome, resolveOmpHome, resolvePiHome } from "../src/client-config.js";
import { SessionStore, _setStoreForTest } from "../src/persist.js";
@@ -317,6 +317,68 @@ test("dsh scan: missing profiles root yields empty result without throwing", ()
assert.deepEqual(res.findings, []);
});
+test("dsh scan: dsh-live-compact is a KNOWN conflict (competing compaction engine, #5984)", () => {
+ clearScanCache();
+ const root = tmp("bili-1206-dsh-dlc-");
+ const env: NodeJS.ProcessEnv = { ...hermeticEnv(root), DSH_HOME: path.join(root, "dsh") };
+ const file = path.join(root, "dsh", "profiles", "main", "package.json");
+ assertTestOwned(file, root);
+ writeFile(file, JSON.stringify({
+ dependencies: {
+ "dsh-live-compact": "^1.1.2",
+ "billion-context": "^0.1.0",
+ },
+ }));
+ const res = scanClientPlugins("dsh", { env, cwd: root });
+ const f = res.findings.find((x) => x.entry === "dsh-live-compact");
+ assert.ok(f, "dsh-live-compact must be flagged");
+ // Build the recorded detail string (same shape as src/server/handle.ts:
+ // recordConflict) BEFORE the assert.equal below narrows f.match —
+ // node:assert/strict's strictEqual carries `asserts actual is T`.
+ const detail = `${f.client}: ${f.entry} (${f.source})${f.match === "keyword" ? " [suspected]" : ""}`;
+ assert.equal(f.match, "known");
+ assert.equal(f.knownId, "dsh-live-compact");
+ // A known finding is never design-benign, even with bili driving dsh —
+ // it is a live competing compressor, unlike the sibling integrations.
+ assert.equal(isDesignBenign(f, "dsh"), false);
+ const d = parsePluginConflictDetail(detail);
+ assert.ok(d, "the recorded detail must parse back");
+ assert.equal(d.entry, "dsh-live-compact");
+ assert.equal(d.suspected, false, "a known finding must not carry the [suspected] tag");
+ assert.ok(!isSiblingConflictDetail(detail), "a dsh finding is not a sibling detail");
+ assert.ok(!isDisplayOnlyConflictDetail(detail), "dsh-live-compact is a live compressor, never display-only");
+});
+
+test("dsh scan: versioned dsh-live-compact npm spec is a KNOWN conflict too (#5984)", () => {
+ clearScanCache();
+ const root = tmp("bili-1206-dsh-dlc-spec-");
+ const env: NodeJS.ProcessEnv = { ...hermeticEnv(root), DSH_HOME: path.join(root, "dsh") };
+ const file = path.join(root, "dsh", "profiles", "main", "package.json");
+ assertTestOwned(file, root);
+ writeFile(file, JSON.stringify({ dependencies: { "dsh-live-compact@1.1.2": "1.1.2" } }));
+ const res = scanClientPlugins("dsh", { env, cwd: root });
+ const f = res.findings.find((x) => x.entry === "dsh-live-compact@1.1.2");
+ assert.ok(f, "the spec-form entry must be flagged");
+ assert.equal(f?.match, "known");
+ assert.equal(f?.knownId, "dsh-live-compact");
+});
+
+test("dsh scan: dsh-live-compact lookalike stays keyword-suspected, not known (#5984)", () => {
+ clearScanCache();
+ const root = tmp("bili-1206-dsh-dlc-lookalike-");
+ const env: NodeJS.ProcessEnv = { ...hermeticEnv(root), DSH_HOME: path.join(root, "dsh") };
+ const file = path.join(root, "dsh", "profiles", "main", "package.json");
+ assertTestOwned(file, root);
+ writeFile(file, JSON.stringify({ dependencies: { "dsh-live-compact-viewer": "^0.1.0" } }));
+ const res = scanClientPlugins("dsh", { env, cwd: root });
+ const f = res.findings.find((x) => x.entry === "dsh-live-compact-viewer");
+ assert.ok(f, "the 'compact' action token still flags the lookalike for user confirmation");
+ assert.equal(f?.match, "keyword");
+ assert.equal(isDshLiveCompactEntry("dsh-live-compact-viewer"), false);
+ assert.equal(isDshLiveCompactEntry("dsh-live-compact"), true);
+ assert.equal(isDshLiveCompactEntry("/home/u/dsh/profiles/main/node_modules/dsh-live-compact"), true);
+});
+
test("claude scan: enabledPlugins keys + plugins dir scanned", () => {
clearScanCache();
const root = tmp("bili-1206-claude-");