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-");