diff --git a/CHANGELOG.md b/CHANGELOG.md index da8caada8f..a097c3b951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Versions follow the merge of a `*_release-v*` branch; CI publishes to npm on tag ## [Unreleased] +### Removed +- **compress-at-fold-point 整功能移除(owner 指令 2026-11-10,#6132;v1 #5471 + 未发布的 v2 #5949)**:删除 `compress.compressAtFoldPoint` / `compress.compressAtFoldPointClearThinking` 配置键、`BILI_COMPRESS_AT_FOLD_POINT` / `BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING` env、kernel 的 `relocate-compress-carriers` 管线节点与 `relocateCompressCarriers` / `planRelocateCompressCarriers` 导出、`Config.reasoningDroppable`、v2 的 `already-folded-echo` 解析 kind、web 配置行与 i18n 文案、config-reference/站点文档与博客文章(owner 指令直接删除)。移除原因:9 天内在其目标模型家族(qwen3-27b 系)出现两次不同的真实事故——2026-11-08 v1 引发的自发重复压缩循环(#5949 现场报告,v2 的设计动机)与 2026-11-10 生产会话(v0.1.213,v1 语义)中的作者误认:同轮「回答 + compress」被重放时,压缩对挪位后该轮回答文本裸挂在 transcript 尾部(无 pending 工具调用、无工具结果、无用户输入),27B 模型把自己的最后一条发言当成用户消息(m00376 链),重复回答了自己的答案且错误认知被固化进折叠块主题。v2 从未发布(最新 tag v0.1.213 仍为 v1,`git tag --contains` 为空),移除零迁移成本:开关关闭时布局就是移除后的布局(字节级一致)。旧配置残留的键:一次性 INERT 警告(`warnInertRemovedCompressKeys`,含 top-level 残留)并忽略值。kernel 0.0.192 → 0.0.193。回归面:`tests/removed-compress-keys-inert.test.ts`(新增移除键用例)、`tests/compress-reject-reasons.test.ts`(删除 echo 用例)、`kernel/tests/parse-compress-input.test.ts`(删除两个 pointer-echo 用例)、`kernel/tests/pipeline.test.ts`(节点清单)。 + ### Changed - **AGENTS.md 瘦身(owner 指令 2026-11-08):76 KB → 50.6 KB(−34%),规则零丢失,编号不变**。KDD 分两级:Tier A(#2/#4/#6/#7/#8/#10,每会话操作规则)留 AGENTS.md 全文;Tier B(#1/#3/#5/#9/#11–#18)全文逐字搬入新文件 `reference/design-decisions.md`(append-only 台账,**KDD 编号 = 稳定公共 API,原样保留**),AGENTS.md 每条留 1–3 行操作性后果 + 指针。Issue 分类与标签规则整节搬入 `reference/triage.md`(p1 口径与总账讨论链接留在正文)。E2E/Release 细则收敛为指向既有 `reference/testing.md` / `reference/release.md` 的硬规则。被反向引用的 heading 锚点(`#4-git-safety-rules-mandatory` 等 6 处)文本原样不动。 - **系统架构文档体系落地(owner 指令 2026-11-08):`docs/architecture/` 成为实时维护的架构枢纽**:新增 `docs/architecture.md` / `docs/architecture.zh-CN.md` 总枢纽(整体架构图中英双版转正 + 8 模块总表),首个样板模块页 `docs/architecture/storage.{md,zh-CN.md}`(统一存储:职责/原理/工作机制/硬不变量,配模块图中英双版)。文件级地图 `reference/architecture.md` 补齐 125 个缺失条目(新存储层 / marker / fastpath / lanes 等全部新模块)并升级为 CI 强制全量覆盖。新增 CI 车道 `architecture-docs`(`tools/check-architecture-docs.mjs`):地图覆盖全 `src/` 文件、总表↔页面成对、图 svg 优先 + png 备选齐备、SVG 基础良构(未转义 `&` 曾让英文图线上破图)。AGENTS.md「Documentation Layout」新增实时维护规则:改结构 ⇒ 同 PR 更新页 + 图 + 总表。其余 7 个模块页(pipeline / lanes / kernel / identity / fastpaths / webui / update)同一 PR 全部补齐——总表 8 行全 ✅(每页 = 中英双版文档 + 中英双版图)。 diff --git a/docs/configuration.md b/docs/configuration.md index 4055124b2f..3e89d041a4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -297,8 +297,6 @@ This index is generated from `website/config-reference/*.yaml` — edit the seed | `compress.maxSummaryLength` | number | kernel 20000 | — | Hard character cap on ONE generated summary; longer replies are rejected whole (no truncation, no retry). External summarizers are told this exact cap up front (#3635). 0 = unbounded. | | `compress.minSummaryLength` | number | kernel 50 | — | Character floor for generated summaries: a single range must meet it alone; a multi-range batch passes when its combined summaries reach it (#5894). 0 = disabled. External summarizers remain held to the per-summary floor (#3635). | | `compress.reconcile` | "off" \| "warn" \| "repair" | "repair" | BILI_FOLD_RECONCILE | Reconcile folded state when the client rewinds or rewrites history between turns. | -| `compress.compressAtFoldPoint` | boolean | false | BILI_COMPRESS_AT_FOLD_POINT | EXPERIMENTAL, default off: move a still-visible compress call/result pair from its original (tail-adjacent) slot to its own fold point in the re-sent view, i.e. [folded region] – [pair] – [protected messages], so weak-attention models stop reading a trailing tool exchange as a fresh command. View-only transform: refs and state untouched, recomputed identically on every turn; off = byte-identical layout. | -| `compress.compressAtFoldPointClearThinking` | boolean | false | BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING | Second opt-in riding on compress.compressAtFoldPoint: when the pair is repositioned, also drop the contiguous run of unsigned reasoning cores immediately preceding it (the model's thinking about this very fold), so the moved pair starts clean. Inert unless the main switch is on; signed thinking blocks and strict reasoning-echo sessions are never touched (#1658/#684). | | `compress.promptPack` | string (builtin: "default", "lean") | builtin "default" | — | Compression prompt pack, resolved project pack → user pack → builtin; not gated by acknowledgePromptsRisk. | | `compress.tiers` | boolean | true | — | Tiered T1→T3 distillation spreads folding cost across generations. | | `compress.protectedTools` | string[] | none | — | Hard exclusion across all history: results of listed tools never fold. Path patterns (skill/) select skills by name (#1947). | @@ -1953,8 +1951,6 @@ File keys resolve only when the matching env var is unset. Defaults in parenthes | `BILI_CLIENT_ERROR_BACKSTOP_MS` | `network.clientErrorBackstopMs` | 30000 | | `BILI_CODEARTS_REF` | `resign` | {} (armed; built-in scheme sdk-hmac-sha256) | | `BILI_CODEX_COMPACT` | `codexCompact` | "intercept" | -| `BILI_COMPRESS_AT_FOLD_POINT` | `compress.compressAtFoldPoint` | false | -| `BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING` | `compress.compressAtFoldPointClearThinking` | false | | `BILI_DECODED_BODY_BYTES` | `network.decodedBodyBytes` | 10737418240 | | `BILI_DECOMPRESS_TMP_CAP` | `decompressTmpCap` | 50 | | `BILI_DSH_AUTHORED_COMPACTION` | `dsh.authoredCompaction` | true (on, #3901 flip) | diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md index 7aa2403927..c5f18d030e 100644 --- a/docs/configuration.zh-CN.md +++ b/docs/configuration.zh-CN.md @@ -297,8 +297,6 @@ | `compress.maxSummaryLength` | number | kernel 20000 | — | 单条生成摘要的硬字符上限;超长的回复被整段拒绝(不截断、不重试)。外部摘要模型会被提前告知这个确切上限(#3635)。0 = 不限制。 | | `compress.minSummaryLength` | number | kernel 50 | — | 生成摘要的字符下限:单段压缩按自身判定;批量压缩按合计判定——一段偷懒不再拖垮整批(#5894)。0 = 禁用。外部摘要模型仍受逐段下限约束(#3635)。 | | `compress.reconcile` | "off" \| "warn" \| "repair" | "repair" | BILI_FOLD_RECONCILE | 客户端在轮次之间回退或改写历史时校准已折叠状态。 | -| `compress.compressAtFoldPoint` | boolean | false | BILI_COMPRESS_AT_FOLD_POINT | 实验性,默认关闭:把仍可见的压缩调用/结果对从原位置(贴近尾部)移到它自己的折叠点,即 [已折叠区]–[调用对]–[保护消息],避免弱注意力模型把历史末尾的工具交换当成新命令。纯视图变换:refs 与状态不变、每轮重算结果一致;关闭时布局字节级不变。 | -| `compress.compressAtFoldPointClearThinking` | boolean | false | BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING | 依附于 compress.compressAtFoldPoint 的第二层开关:调用对被挪位的一瞬间,同时丢掉紧邻其前的未签名思考段(模型对本次折叠本身的思考),让挪位后的开头更干净。主开关关闭时无效;带签名的思考块与严格推理回显会话绝不受影响(#1658/#684)。 | | `compress.promptPack` | string (builtin: "default", "lean") | builtin "default" | — | 压缩提示词包,按 项目包 → 用户包 → 内置 解析;不受 acknowledgePromptsRisk 门控。 | | `compress.tiers` | boolean | true | — | T1→T3 分级蒸馏把折叠成本摊到多代。 | | `compress.protectedTools` | string[] | none | — | 全历史硬排除:列出工具的结果永不折叠。路径模式(skill/)可按名指定 skill(#1947)。 | @@ -1930,8 +1928,6 @@ ACP 原生 agent(当前为 `pi` 扩展)会在每个进程内向代理上报 | `BILI_CLIENT_ERROR_BACKSTOP_MS` | `network.clientErrorBackstopMs` | 30000 | | `BILI_CODEARTS_REF` | `resign` | {} (armed; built-in scheme sdk-hmac-sha256) | | `BILI_CODEX_COMPACT` | `codexCompact` | "intercept" | -| `BILI_COMPRESS_AT_FOLD_POINT` | `compress.compressAtFoldPoint` | false | -| `BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING` | `compress.compressAtFoldPointClearThinking` | false | | `BILI_DECODED_BODY_BYTES` | `network.decodedBodyBytes` | 10737418240 | | `BILI_DECOMPRESS_TMP_CAP` | `decompressTmpCap` | 50 | | `BILI_DSH_AUTHORED_COMPACTION` | `dsh.authoredCompaction` | true (on, #3901 flip) | diff --git a/docs/lifecycle.md b/docs/lifecycle.md index 443e11bfde..f7109a291a 100644 --- a/docs/lifecycle.md +++ b/docs/lifecycle.md @@ -54,8 +54,6 @@ | `BILI_OPENCODE_INLINE` | #3278 opencode 移植(#4687) | opencode inline 模式(需手动开启,默认保持旧行为 spawn,owner 决定不改默认):与 pi 同构——不起 proxy 进程,模型 URL 被 V2 route/intercept 改写成伪 origin 后由进程内引擎接住(bun 宿主已验:引擎 boot/dispatch 全绿);`=1/on/yes/TRUE` 或 file 家 `"opencode": {"inline": true}` 显式开启(显式开启压过 attach 目标);`bili opencode-inline` 一键车道 | 同 pi inline 车道节奏 | opt-in(不设置时 attach/spawn 行为与旧版完全一致) | | `BILI_INLINE_DEBUG` | #4669 | 仅 inline 车道:引擎 stderr 日志腿默认静默(stderr 是 pi 的交互终端,日志会挤掉输入框;默认只写 bili.log),设 1 恢复 stderr 输出用于排查 | 随 inline 车道(#3278)走 | 排查逃生口 | | 测试钩子:`BILI_DSH_RECOVERY_INTERVAL_MS` / `BILI_DSH_RETRY_INTERVAL_MS` / `BILI_MODEL_INFO_RETRY_MS` | #2082 等 | 缩短测试/运维节奏(dsh-native.ts;NODE_TEST_CONTEXT 感知,避免生产误缩) | 永久测试设施,随宿主代码走 | 测试钩子 | -| `BILI_COMPRESS_AT_FOLD_POINT` | #5470 | compress-at-fold-point 实验性 opt-in(**默认关**;2026-11-01 真实模型验证通过,owner 决定保持 opt-in):把重放视图中可见的 compress 调用对挪到它自己的折叠点(布局 [已折叠区]–[调用对]–[保护消息]),弱注意力模型不再把历史尾部的工具交换误读成新命令;纯视图变换,关时布局字节级不变;文件键 `compress.compressAtFoldPoint`;含 env-only opt-in 可达性修复(默认短路保持不变) | 更多真实模型积累后转正为默认开(走评审),或按废弃流程删除 | opt-in(默认关) | -| `BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING` | #5470 | 依附上一开关:挪位瞬间清紧邻的未签名思考段(默认 off,A 关时无效);strict-echo(#684) 或存在签名思考块(#1658)时整个机制不出手 | 同上 | opt-in(默认 off) | | `BILI_PF_RENDER_WINDOW` / `BILI_PF_RENDER_AUDIT_EVERY` | #5664 v6 render-snapshot plane | 实验性渲染快照车道可调项(默认 OFF,嵌套于 watermarkV6MsgId cohort):W = 稳态轮回放的冻结核数(默认 8);N = 计划全量审计节奏(默认 8);读点 src/server/fastpath-v6-render.ts:50-60 | render 平面转正(rollout 100% 且审计计数归稳)后移 §2 常设面,或随车道退役删除 | opt-in(默认 OFF;仅由显式 rollout 开启) | | `BILI_IDENTITY_HARVEST` | #5825 / PR #5842 | 身份信道采集 kill/scope 开关(src/identity-harvest.ts):缺省=OFF(接线以惰性状态出厂);`1`/`all`=全部已登记来源;逗号列表=按 roster 名圈定;须与 rollout `pluginLaneServerHistory` 及仅回环来源同时放行才生效 | harvest 车道(claude-code/codebuddy/mcode)站稳后按 #2028 模式退役 env(转 file 键)或移 §2 | opt-in(默认 off;rollout 0% 起灰度) | @@ -238,6 +236,7 @@ ## 5. 卫生记录 +- **2026-11-10 compress-at-fold-point 整功能移除(owner 指令,#6132)**:实验性开关 `BILI_COMPRESS_AT_FOLD_POINT` / `BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING`(§1 两行)及文件键 `compress.compressAtFoldPoint` / `compress.compressAtFoldPointClearThinking` 整体删除(v1 #5471 + 未发布的 v2 #5949)。移除原因:9 天内在其目标模型家族上出现两次不同的真实事故——2026-11-08 v1 引发的自发重复压缩循环(#5949 现场报告)与 2026-11-10 v0.1.213(v1 语义)生产会话中的作者误认(模型把自己最后一句话当成用户消息,详见 #6132)。旧配置中残留的键会被一次性 INERT 警告(warnInertRemovedCompressKeys)并忽略值;博客文章按 owner 指令直接删除。 - **2026-11-08 文档集中化(owner 指令 2026-11-08)**:14 个根级设计/配置文档一次性迁入 `docs/`(CONFIGURATION→`docs/configuration`、CLIENTS→`docs/clients`、TECHNICAL-NOTES→`docs/technical-notes`、MESSAGE-IDENTITY→`docs/message-identity`、SESSION-IDENTITY→`docs/session-identity`、PLUGIN→`docs/plugin-protocol`、LIFECYCLE→`docs/lifecycle`、UNIFIED_LOOP_SPEC→`docs/unified-loop-spec`、AUTO-MERGE-GUARDRAILS→`docs/auto-merge-guardrails`,含 zh-CN 对);新 `docs/index.md` / `docs/index.zh-CN.md` 为文档总目录。原路径留单行重定向占位,**解除条件:1–2 个版本后且无外部链接反馈时删除占位**(届时同步删根级 stub)。CI `docs-sync` diff 路径与 `scripts/{config-surface-guard,lifecycle-ledger-guard}.mjs` 运行时路径、`package.json` files 数组已随迁。 - **2026-10-15 远端分支卫生(#3249 执行)**:删除 origin 已合并远端分支 **1394 条**(1386 条 = 已合并 PR 的同仓 head 分支 + 8 条 = 完全并入 master 但无对应 PR 记录:`2026-08-08_release-v0.1.14`、`2026-09-26_opencode-zen-mitm`、`2026-09-30_responses-side-compaction`、`2026-10-04_plugin-fork-context-provenance`、`2026-10-13_lastmessages-tabling`、`chore/pages-enablement`、`fix/codex-custom-tool-call-text-protocol`、`revert-3189-pages-enablement`)。删除前核对:与 open PR head 交集为空。删除后 origin 剩 361 个 head(其中 128 条为 open PR 分支、201 条 closed-unmerged 且未并入 master、30 条无 PR 未并入 = 废弃工作,均不动)。另执行一次 `git worktree prune` 清掉失效 worktree 登记。 - **2026-10-20 台账补登(本 PR)**:#3249 落地本台账时 `src/` 实际读取数已比登记数多 22(#3955 mysql、guardrails、#3128 网络预算、#3400 图像缩放、#3619 错峰、#3733 dsh compaction、#3756 snapshot shadow 等),守卫上线即全线红(master run 均被后续 push 取消所以未暴露)。本次 22 条全部补登(3 条 §1 + 19 条 §2)。 diff --git a/kernel/package-lock.json b/kernel/package-lock.json index faf2a36220..616a2835a4 100644 --- a/kernel/package-lock.json +++ b/kernel/package-lock.json @@ -1,12 +1,12 @@ { "name": "acp-kernel", - "version": "0.0.192", + "version": "0.0.193", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "acp-kernel", - "version": "0.0.192", + "version": "0.0.193", "license": "MIT", "devDependencies": { "@types/node": "^22.0.0", diff --git a/kernel/package.json b/kernel/package.json index d5c9064802..b8709a1de6 100644 --- a/kernel/package.json +++ b/kernel/package.json @@ -1,6 +1,6 @@ { "name": "acp-kernel", - "version": "0.0.192", + "version": "0.0.193", "description": "Framework-agnostic context-compression engine (model-driven, 3-tier LSM). Pure core: no host dependency.", "license": "MIT", "author": "ranxianglei", diff --git a/kernel/src/compress.ts b/kernel/src/compress.ts index 2956d7b607..f2d7ee1e26 100644 --- a/kernel/src/compress.ts +++ b/kernel/src/compress.ts @@ -19,7 +19,6 @@ import { appendAbsorbPrompts, hideAbsorbedMessages } from "./absorb.js"; import { applyCrushToMessages } from "./crush.js"; import { applyRetrieve, ccrStoreNode, RETRIEVED_ID_PREFIX } from "./ccr.js"; import { preTransformSnapshotNode, viewLatchNode } from "./view-latch.js"; -import { relocateCompressCarriers } from "./relocate-compress.js"; import type { ApplyRetrieveResult, CcrEffect } from "./ccr.js"; import type { EmergencyTruncateLatchEntry } from "./types.js"; import { createContentStore } from "./content-store.js"; @@ -1144,11 +1143,6 @@ export function createCore(ports: Ports = {}): CompressionCore { absorbPromptNode, filterNode, hideCompressCallsNode, - // EXPERIMENTAL, default off (Config.compressAtFoldPoint): move still-visible - // carrier pairs onto their fold point. Must run AFTER hide-compress-calls - // (whose orphan sweep just settled which carriers survive) and BEFORE - // view-latch so the latch records the relocated bytes. - relocateCompressCarriersNode, viewLatchNode, recommendNode, nudgeNode, @@ -1463,22 +1457,6 @@ const hideCompressCallsNode: PipelineNode = { }, }; -const relocateCompressCarriersNode: PipelineNode = { - name: "relocate-compress-carriers", - run(io, ctx) { - const cfg = ctx.config; - if (!cfg.compressAtFoldPoint) return io; - return { - ...io, - messages: relocateCompressCarriers(io.messages, io.state, { - isCompressTool: cfg.isCompressTool, - clearThinking: cfg.compressAtFoldPointClearThinking === true, - reasoningDroppable: cfg.reasoningDroppable, - }), - }; - }, -}; - const recommendNode: PipelineNode = { name: "recommend", run(io, ctx) { diff --git a/kernel/src/index.ts b/kernel/src/index.ts index 77e0a68b99..9b6e75ffcc 100644 --- a/kernel/src/index.ts +++ b/kernel/src/index.ts @@ -158,10 +158,6 @@ export type { HandoffInput, HandoffMeta, HandoffBlockFull } from "./handoff.js"; export { isToolMessage } from "./message-kind.js"; export { hideConsumedCompressCalls } from "./hide-consumed.js"; export type { HideConsumedResult } from "./hide-consumed.js"; -// Experimental fold-point carrier relocation (default off) — host-side mirror -// sites (round-2 re-render, S4 plugin render) call the same pure function. -export { relocateCompressCarriers, planRelocateCompressCarriers } from "./relocate-compress.js"; -export type { RelocateOptions, RelocatePlan, RelocateMove } from "./relocate-compress.js"; export { ABSORB_TOOL_NAME, ABSORB_TOOL, diff --git a/kernel/src/parse-compress-input.ts b/kernel/src/parse-compress-input.ts index fa96b7a947..7ebc49c20a 100644 --- a/kernel/src/parse-compress-input.ts +++ b/kernel/src/parse-compress-input.ts @@ -27,20 +27,12 @@ import { clampPrefix } from "./truncate.js"; import type { CompressRangeSpec } from "./types.js"; -// The ack text compressAtFoldPoint v2 writes into a completed compress call's -// rewritten args (see relocate-compress.ts on the branch that introduced it; -// mirrored here as a literal so this parser change lands independently of -// that one — keep the two strings identical when both are on master). -const RELOCATED_POINTER_MARKER = - "[bili] compression succeeded — summary relocated"; - export type CompressParseKind = | "ok" | "empty-input" | "not-object" | "missing-content" | "content-not-array" - | "already-folded-echo" | "malformed-json" | "truncated" | "no-valid-ranges"; @@ -203,17 +195,6 @@ function parseObjectValue( if (Array.isArray(content)) { entries = content; } else if (typeof content === "string") { - // compressAtFoldPoint v2 rewrites a completed compress call's args to a - // short ack pointer (relocate-compress.ts). A model that copies its most - // recent compress call as a template re-sends that pointer text as the - // content of a NEW call — observed live as bursts of identical - // content-not-array rejections, each receipt feeding the next retry. - // Classify it explicitly so hosts can ack a no-op instead of lecturing - // a format that was never wrong. - if (content.includes(RELOCATED_POINTER_MARKER)) { - diag.kind = "already-folded-echo"; - return finish([], diag); - } // Stringified content array: vLLM-style gateways stringify nested // arrays, so `content` arrives as a JSON string of the array. const parsed = parseContentArray(content); diff --git a/kernel/src/relocate-compress.ts b/kernel/src/relocate-compress.ts deleted file mode 100644 index 827eb88919..0000000000 --- a/kernel/src/relocate-compress.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { BLOCKED_REF } from "./refs.js"; -import { refNum } from "./hide-consumed.js"; -import type { CompressionState, CoreMessage } from "./types.js"; - -/** Opt-in view transform (experimental, default OFF — see Config.compressAtFoldPoint): - * relocate the SUMMARY TEXT of each still-visible compress carrier pair to the - * fold point, leaving the pair itself in its chronological slot with the call - * args replaced by a short pointer. - * - * Semantics (v2, #5470 follow-up — replaces the verbatim-pair move): when the - * knob is on, for each compress call/result pair that still owns an active - * block: - * - a synthetic `user` text core carrying the call's summary text (the args - * `content`, or the raw args string when not JSON) is inserted directly - * after the pair's own folded region ("[folded region] – [summary carrier] – - * [protected messages]"); - * - the pair STAYS at its original (chronological) position — the tool - * exchange keeps reading as history, not as a fresh command — with the call - * core's args text replaced by a short pointer ("compression succeeded; - * summary relocated to its fold point above") so a weak-attention model - * cannot mistake the pair for an unfinished compression. The result core is - * forwarded verbatim. - * - * Why v2: with the v1 verbatim move, every historical pair stays visible - * mid-history as a fully-shaped compress interaction — for weak-attention - * models that gallery reads as an open invitation to keep compressing - * (observed: voluntary re-compress loops). v2 keeps exactly one recent - * interaction shape per fold (the pointer pair, which explicitly says - * "already folded") and renders the payloads the model actually needs to - * re-read (the summaries) as plain text at the place they describe. - * - * Invariants: - * - VIEW-ONLY + deterministic f(history, state): nothing is persisted, refs are - * untouched (assign-refs already ran; messageRefs is a persistent id→ref map), - * so the layout is recomputed identically on every turn — byte-stable from the - * second turn on, no incremental prefix-cache cost beyond the fold's own break. - * The call core is rewritten via a shallow CLONE (input cores are never - * mutated — callers reuse them for persistence and round-2 rebuilds). - * - Idempotent: a call id whose carrier (`acp_reloc_`) is already - * present, or whose args already carry the relocation marker, is skipped — - * re-running the transform on its own output is a same-reference no-op. - * - Pair integrity: a call is rewritten only when BOTH cores are present, in - * order, adjacent, and unambiguous (exactly one tool-call core per active - * block's call id). Any anomaly → the pair stays verbatim (safe direction). - * - Boundary rule: insert the carrier before the first surviving core whose - * allocated ref is strictly greater than max(endRef) across the blocks this - * call created (multi-range batches → rightmost boundary); when none exists - * (fold reaches the window end, #4732 regime) append at the tail instead. - * - Thinking clear (second opt-in, default off, predicate-gated): the run of - * contiguous `reasoning` cores immediately preceding the pair may be dropped - * IF opts.reasoningDroppable accepts each one. Without a predicate - * NOTHING is dropped — signed-thinking / strict-echo awareness lives in the - * host (it owns the wire signatures), and the conservative default here is - * "never drop". */ -export interface RelocateOptions { - /** Same contract as Config.isCompressTool: host dialect names count as a - * compress call. Unset = bare-name only. */ - isCompressTool?: (toolName: string) => boolean; - /** Enable the #651-style reasoning drop around kept pairs. */ - clearThinking?: boolean; - /** Host predicate deciding which reasoning cores may be dropped (signed - * thinking and strict-echo sessions must answer false). UNSET ⇒ no drop even - * with clearThinking set. Must be pure and cheap: runs once per candidate. */ - reasoningDroppable?: (msg: CoreMessage) => boolean; -} - -export interface RelocateMove { - callId: string; - /** Index of the tool-call core in the input array. */ - callIndex: number; - /** Index of the tool-result core in the input array (stays in place, verbatim). */ - resultIndex: number; - /** Survivor ordinal: emit the carrier before the k-th surviving (non-removed) - * input. k == total survivors ⇒ tail append (#4732 regime). */ - insertAtOutput: number; - /** Input indices of dropped reasoning cores (adjacent run before the call). */ - droppedReasoningIndices: number[]; - /** Synthetic fold-point carrier carrying the relocated summary text. - * `summaryOfBlockId` anchors the #335 live-checkpoint-carrier protection - * (plain compress ranges cannot fold it; block-ref distillation can). */ - carrier: CoreMessage; - /** Replacement args text for the call core (pointer JSON). */ - pointerText: string; -} - -export interface RelocatePlan { - moves: RelocateMove[]; - /** Input indices of ALL cores removed (dropped reasoning only — pairs stay). */ - removedInputIndices: Set; - changed: boolean; -} - -const bareIsCompressTool = (toolName: string | undefined): boolean => - toolName === "compress"; - -/** Marker embedded in the pointer args text; also the idempotency detector for - * hosts that round-trip a view whose carrier ids did not survive. */ -export const RELOCATED_POINTER_MARKER = - "[bili] compression succeeded — summary relocated"; - -const carrierIdFor = (callId: string): string => `acp_reloc_${callId}`; - -/** Pull the summary text out of a compress call's args. The plugin dialect - * rides JSON `{"content": "…"}`; anything else is used verbatim. Pure. */ -function summaryTextOf(callText: string | undefined): string { - if (!callText) return ""; - try { - const parsed: unknown = JSON.parse(callText); - if ( - typeof parsed === "object" && parsed !== null && - typeof (parsed as { content?: unknown }).content === "string" - ) { - return (parsed as { content: string }).content; - } - } catch { - // not JSON — raw args are the summary - } - return callText; -} - -/** Compute the relocation plan for `messages` under `state`. Pure: same inputs - * always yield the same plan (host-side determinism requirement). */ -export function planRelocateCompressCarriers( - messages: CoreMessage[], - state: CompressionState, - opts: RelocateOptions = {}, -): RelocatePlan { - const isCompressTool = opts.isCompressTool ?? bareIsCompressTool; - const byRaw = state.messageRefs.byRaw; - - // 1. Which call ids still own an ACTIVE block whose boundaries are numeric - // (legacy blocks lack start/endRef — they cannot get a boundary, skip). - const endRefNumByCallId = new Map(); - const blockIdsByCallId = new Map(); - for (const block of state.blocks) { - if (!block.active || !block.compressCallId) continue; - const n = refNum(block.endRef); - if (n === null) continue; // legacyLiveByCallId-era or unresolved boundary - const prev = endRefNumByCallId.get(block.compressCallId) ?? 0; - if (n > prev) endRefNumByCallId.set(block.compressCallId, n); - const ids = blockIdsByCallId.get(block.compressCallId); - if (ids === undefined) blockIdsByCallId.set(block.compressCallId, [block.blockId]); - else ids.push(block.blockId); - } - if (endRefNumByCallId.size === 0) { - return { moves: [], removedInputIndices: new Set(), changed: false }; - } - - const refOf = (m: CoreMessage): number | null => { - const ref = byRaw[m.id]; - return ref === undefined || ref === BLOCKED_REF ? null : refNum(ref); - }; - - // Idempotency probes (view-only transform may be re-run on its own output - // by the relay round-2 mirror and the plugin-render mirror). - const presentCarrierIds = new Set(); - for (const m of messages) presentCarrierIds.add(m.id); - - // 2. Locate candidate pairs (one pass; duplicates disqualify their call id). - const callsByCallId = new Map(); - for (let i = 0; i < messages.length; i++) { - const m = messages[i]!; - if (m.contentType !== "tool-call" || !m.toolCallId) continue; - if (!isCompressTool(m.toolName ?? "")) continue; - if (!endRefNumByCallId.has(m.toolCallId)) continue; - const list = callsByCallId.get(m.toolCallId); - if (list === undefined) callsByCallId.set(m.toolCallId, [i]); - else list.push(i); - } - - const moves: RelocateMove[] = []; - const removed = new Set(); - for (const [callId, callIndices] of [...callsByCallId.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)) { - if (callIndices.length !== 1) continue; // ambiguous carrier: do not touch - const j = callIndices[0]!; - // Already relocated in a previous pass of this view? Skip entirely. - if (presentCarrierIds.has(carrierIdFor(callId))) continue; - if ((messages[j]!.text ?? "").includes(RELOCATED_POINTER_MARKER)) continue; - const resultIndex = j + 1; - const r = messages[resultIndex]; - if (!r || r.contentType !== "tool-result" || r.toolCallId !== callId) continue; - // Adjacent ordered pair confirmed. - const blockIds = blockIdsByCallId.get(callId) ?? []; - const blockLabel = blockIds.length > 0 ? `block(s) ${blockIds.join(", ")}` : "fold"; - const summary = summaryTextOf(messages[j]!.text); - const carrier: CoreMessage = { - id: carrierIdFor(callId), - role: "user", - contentType: "text", - // #335 checkpoint-carrier protection: while the block(s) this summary - // describes are still active, a PLAIN message-ref compress range can - // neither be advertised across this carrier (recommend.ts screens with - // isLiveCheckpointCarrier) nor fold it (unit-projection carves it out - // and keeps it visible) — a relocated summary must not be re-distilled - // as if it were ordinary user text, because the history it summarizes - // is already folded away. Once the anchor block is consumed by a - // block-ref distillation (which sees and absorbs the carrier text), - // this turns stale and the carrier folds like ordinary text — that is - // the intended "the RESULT may be compressed later" path. Multi-block - // pairs anchor the FIRST block: an active anchor keeps the protection, - // and distillations that consume the anchor virtually always span the - // sibling blocks created by the same call. - summaryOfBlockId: blockIds[0], - text: - `[bili fold summary — ${blockLabel}; relocated from compress call ${callId} ` + - `(the call/result exchange stays in its chronological slot below)]\n${summary}`, - }; - const pointerText = JSON.stringify({ - content: - `${RELOCATED_POINTER_MARKER} to its fold point above (${blockLabel}); ` + - `these ranges are already folded — do not re-compress them.`, - }); - // 3. Optional thinking clear: contiguous reasoning run directly before the - // pair, predicate-gated per core (predicate unset ⇒ drop nothing). - const droppedReasoningIndices: number[] = []; - if (opts.clearThinking && typeof opts.reasoningDroppable === "function") { - for (let k = j - 1; k >= 0; k--) { - const c = messages[k]!; - if (c.contentType !== "reasoning") break; - if (opts.reasoningDroppable(c)) { - droppedReasoningIndices.push(k); - removed.add(k); - } - } - } - moves.push({ callId, callIndex: j, resultIndex, insertAtOutput: 0, droppedReasoningIndices, carrier, pointerText }); - } - if (moves.length === 0) { - return { moves: [], removedInputIndices: new Set(), changed: false }; - } - - // 4. Resolve each insertion point against the INPUT array, then convert to - // output space (output = input minus `removed`). A qualifying survivor is - // any non-removed core whose ref strictly exceeds this call's boundary — - // chronology of allocated refs equals ingest order (monotonic mint). The - // kept pair cores themselves carry no refs (tool-call/result cores are not - // ref-minted), so they never qualify as boundaries. - for (const move of moves) { - const boundary = endRefNumByCallId.get(move.callId)!; - let at = messages.length; // tail fallback (#4732 regime) - for (let i = 0; i < messages.length; i++) { - if (removed.has(i)) continue; - const n = refOf(messages[i]!); - if (n !== null && n > boundary) { - at = i; - break; - } - } - let out = 0; - for (let i = 0; i < at; i++) { - if (!removed.has(i)) out++; - } - move.insertAtOutput = out; - } - // Determinism: order by call id so the plan does not depend on Map - // iteration order (two calls folding different ranges may otherwise - // resolve to insertion points in the OPPOSITE order of their call slots). - moves.sort((a, b) => (a.callId < b.callId ? -1 : 1)); - return { moves, removedInputIndices: removed, changed: true }; -} - -/** Apply the plan to `messages`, returning a NEW array when anything changed. - * Fast path: no-op plans return the SAME array reference (byte-identical - * guarantee for the default-off regime). - * - * Placement keys on the SURVIVOR ordinal, not the raw output length: a - * carrier "with insertAtOutput k" must appear right BEFORE the k-th surviving - * input. Emitted carriers grow the output array between survivors, so keying on - * out.length would skip slots whenever an earlier carrier landed first (two - * calls folding different ranges may legally interleave). - * - * The rewritten call core is a shallow CLONE of the input (view-only purity: - * callers reuse input cores for persistence and round-2 rebuilds); every other - * core is forwarded by reference. */ -export function relocateCompressCarriers( - messages: CoreMessage[], - state: CompressionState, - opts: RelocateOptions = {}, -): CoreMessage[] { - const plan = planRelocateCompressCarriers(messages, state, opts); - if (!plan.changed) return messages; - const dueAt = new Map(); - const rewrite = new Map(); // input index → pointer args - for (const move of plan.moves) { - const list = dueAt.get(move.insertAtOutput); - if (list === undefined) dueAt.set(move.insertAtOutput, [move]); - else list.push(move); - rewrite.set(move.callIndex, move.pointerText); - } - // Per-slot order: earliest call slot first (deterministic tie-break when - // several carriers share one insertion point). - for (const list of dueAt.values()) { - list.sort((a, b) => a.callIndex - b.callIndex); - } - const out: CoreMessage[] = []; - const flushDue = (k: number): void => { - const due = dueAt.get(k); - if (due === undefined) return; - dueAt.delete(k); - for (const move of due) out.push(move.carrier); - }; - let survivors = 0; - for (let i = 0; i < messages.length; i++) { - if (plan.removedInputIndices.has(i)) continue; - flushDue(survivors); - const m = messages[i]!; - const pointer = rewrite.get(i); - out.push(pointer === undefined ? m : { ...m, text: pointer }); - survivors++; - } - flushDue(survivors); // tail fallback (#4732 regime) - return out; -} diff --git a/kernel/src/types.ts b/kernel/src/types.ts index 7d4daaf365..d2d75dc7e7 100644 --- a/kernel/src/types.ts +++ b/kernel/src/types.ts @@ -624,27 +624,6 @@ export interface Config { rules?: RulesConfig; /** Deterministic tool-result compression, tier 1 of the absorb gate (see crush.ts). Requires absorb enabled. */ crush?: CrushConfig; - /** EXPERIMENTAL (default off): relocate still-visible compress carrier pairs - * from their original tail-adjacent position to their own fold point — - * "[folded/summary region] – [compress pair] – [protected messages]". - * View-only + deterministic (see relocate-compress.ts): nothing is persisted, - * refs are untouched, so the layout recomputes identically every turn. OFF - * = byte-identical to before (the node fast-paths to the same array ref). - * The round-2 re-render and the S4 plugin render mirror this transform host-side - * with the same pure function so all lanes agree. */ - compressAtFoldPoint?: boolean; - /** Second opt-in riding on compressAtFoldPoint: drop the contiguous `reasoning` - * core run immediately preceding a moved pair — ONLY cores accepted by - * reasoningDroppable. Default off; both flags must be set to drop anything. */ - compressAtFoldPointClearThinking?: boolean; - /** Host predicate for the above drop: signed-thinking blocks and strict-echo - * sessions must answer false (wire signatures live host-side, #1658/#684 - * doctrine). UNSET ⇒ no reasoning core is ever dropped, even with - * compressAtFoldPointClearThinking set (conservative default). Must be pure - * and cheap: runs once per candidate per pass. NOTE: function fields are not - * carried into session effectiveConfig stamps (#2584) — wire paths re-resolve - * it per request; panel/tool reads see it absent by design. */ - reasoningDroppable?: (msg: CoreMessage) => boolean; /** Lossless content-cached retrieval for oversized tool results (see ccr.ts). Absent/disabled = feature off. */ ccr?: CcrConfig; messageFilters?: import("./filter/types.js").MessageFiltersConfig; diff --git a/kernel/tests/parse-compress-input.test.ts b/kernel/tests/parse-compress-input.test.ts index 1e53987642..2e548cc831 100644 --- a/kernel/tests/parse-compress-input.test.ts +++ b/kernel/tests/parse-compress-input.test.ts @@ -1388,28 +1388,9 @@ test("parseCompressArgs refuses to narrow a ref pair with an unrecognized separa }); // --------------------------------------------------------------------------- -// compressAtFoldPoint v2 pointer echo + stringified single entry (#5949 field) +// stringified single entry (#5949 field) // --------------------------------------------------------------------------- -test("content echoing the v2 relocation pointer classifies as already-folded-echo", () => { - // Field shape (billion-context session 01a1b6b3): the model copied the most - // recent compress call's rewritten args — the v2 ack pointer — as the - // content of a NEW call, wrapped in one more JSON layer. - const parsed = parseCompressArgs({ - content: '{"content": "[bili] compression succeeded — summary relocated to its fold point above (block(s) b49); these ranges are already folded — do not re-compress them."}', - }); - assert.equal(parsed.diagnostics.ok, false); - assert.equal(parsed.diagnostics.kind, "already-folded-echo"); - assert.equal(parsed.ranges.length, 0); -}); - -test("bare pointer text in content is already-folded-echo too", () => { - const parsed = parseCompressArgs({ - content: "[bili] compression succeeded — summary relocated to its fold point above (block(s) b50); these ranges are already folded — do not re-compress them.", - }); - assert.equal(parsed.diagnostics.kind, "already-folded-echo"); -}); - test("content string encoding ONE entry object is salvaged (refs inside the JSON)", () => { const parsed = parseCompressArgs({ content: JSON.stringify({ startId: "m00150", endId: "m00220", summary: "investigated the loop; fixed the gate" }), diff --git a/kernel/tests/pipeline.test.ts b/kernel/tests/pipeline.test.ts index 6800fdec1a..6af03e131b 100644 --- a/kernel/tests/pipeline.test.ts +++ b/kernel/tests/pipeline.test.ts @@ -37,7 +37,6 @@ test("defaultNodes exposes the canonical ordered pipeline", () => { "absorb-prompt", "filter", "hide-compress-calls", - "relocate-compress-carriers", "view-latch", "recommend", "nudge-inject", diff --git a/kernel/tests/relocate-compress.test.ts b/kernel/tests/relocate-compress.test.ts deleted file mode 100644 index 56011a79d9..0000000000 --- a/kernel/tests/relocate-compress.test.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { createInitialState, isLiveCheckpointCarrier } from "../src/state.js"; -import { - planRelocateCompressCarriers, - relocateCompressCarriers, - RELOCATED_POINTER_MARKER, -} from "../src/relocate-compress.js"; -import type { - CompressionBlock, - CompressionState, - CoreMessage, -} from "../src/types.js"; - -/** Build a core message. Refs are assigned via state.messageRefs below. */ -function msg( - id: string, - ref: string | null, - contentType: CoreMessage["contentType"], - extra: Partial = {}, -): CoreMessage { - return { - id, - role: "assistant", - contentType, - text: `text-of-${id}`, - ...extra, - }; -} - -function stateWith( - blocks: Array>, - refs: Record, -): CompressionState { - const state = createInitialState(); - for (const [i, b] of blocks.entries()) { - state.blocks.push({ - blockId: b.blockId ?? `b${i}`, - runId: b.runId ?? `r${i}`, - tier: 1, - summary: "summary", - directMessageIds: [], - effectiveMessageIds: [], - directBlockIds: [], - createdAt: 1000 + i, - survivedCount: 0, - generation: "young", - active: true, - ...b, - }); - } - for (const [id, ref] of Object.entries(refs)) { - state.messageRefs.byRaw[id] = ref; - if (ref !== "BLOCKED") state.messageRefs.byRef[ref] = id; - } - return state; -} - -function pair(callId: string) { - const call = msg( - `call-${callId}`, - null, - "tool-call", - { toolName: "compress", toolCallId: callId }, - ); - const result = msg( - `res-${callId}`, - null, - "tool-result", - { toolCallId: callId }, - ); - return [call, result] as const; -} - -const ids = (ms: CoreMessage[]): string[] => ms.map((m) => m.id); -const carrierId = (callId: string): string => `acp_reloc_${callId}`; - -test("no active block with numeric endRef ⇒ same array reference (no-op fast path)", () => { - const messages = [msg("a", "m00001", "text")]; - const state = stateWith([], {}); - assert.equal(relocateCompressCarriers(messages, state), messages); -}); - -test("legacy block without endRef is skipped (cannot get a boundary)", () => { - const [c, r] = pair("call1"); - const messages = [msg("a", "m00001", "text"), c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", summary: "s", startRef: undefined, endRef: undefined }], - {}, - ); - // refs map irrelevant here; block lacks endRef → no candidate call id at all - delete (state.blocks[0] as { startRef?: string }).startRef; - assert.equal(relocateCompressCarriers(messages, state), messages); -}); - -test("inactive block is skipped even with numeric endRef", () => { - const [c, r] = pair("call1"); - const messages = [msg("a", "m00001", "text"), c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", active: false, startRef: "m00001", endRef: "m00002" }], - {}, - ); - assert.equal(relocateCompressCarriers(messages, state), messages); -}); - -test("basic relocate: summary carrier lands at the fold point, pair stays in its slot with pointer args", () => { - // Fold covered m00001–m00002; protected survivors a (m00003), b (m00004); the - // pair sits chronologically AFTER the survivors (the model compressed older - // history later on). - const a = msg("a", "m00003", "text"); - const b = msg("b", "m00004", "text"); - const [c, r] = pair("call1"); - const messages = [a, b, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003", b: "m00004" }, - ); - const out = relocateCompressCarriers(messages, state); - // Carrier before the first survivor past the boundary (a, ref 3 > 2); the - // pair keeps its chronological slot at the end. - assert.deepEqual(ids(out), [carrierId("call1"), "a", "b", "call-call1", "res-call1"]); - // Survivors and the result core are forwarded by reference, never cloned. - assert.equal(out[1], a); - assert.equal(out[2], b); - assert.equal(out[4], r); - // The call core is a shallow CLONE carrying the pointer args. - assert.notEqual(out[3], c); - assert.ok(out[3]!.text!.includes(RELOCATED_POINTER_MARKER)); - assert.ok(out[3]!.text!.includes("b1")); - // The input core is not mutated. - assert.equal(c.text, "text-of-call-call1"); -}); - -test("carrier text carries the summary; non-JSON args are used verbatim", () => { - const a = msg("a", "m00003", "text"); - const [c, r] = pair("call1"); - c.text = "RAW-SUMMARY-BODY"; - const messages = [a, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - const out = relocateCompressCarriers(messages, state); - const carrier = out[0]!; - assert.equal(carrier.id, carrierId("call1")); - assert.equal(carrier.role, "user"); - assert.equal(carrier.contentType, "text"); - assert.ok(carrier.text!.startsWith(`[bili fold summary — block(s) b1; relocated from compress call call1`)); - assert.ok(carrier.text!.endsWith("\nRAW-SUMMARY-BODY")); - // #335 protection: the carrier is a live checkpoint carrier while b1 is - // active — plain compress ranges can neither span nor fold it. - assert.equal(carrier.summaryOfBlockId, "b1"); - assert.equal(isLiveCheckpointCarrier(carrier, state), true); -}); - -test("carrier text extracts .content from JSON args (plugin dialect)", () => { - const a = msg("a", "m00003", "text"); - const [c, r] = pair("call1"); - c.text = JSON.stringify({ content: "SUMMARY-TEXT", ranges: ["m00001-m00002"] }); - const messages = [a, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - const out = relocateCompressCarriers(messages, state); - const carrier = out[0]!; - assert.ok(carrier.text!.includes("SUMMARY-TEXT")); - assert.ok(!carrier.text!.includes("SUMMARY-TEXT\"")); // no JSON wrapper residue -}); - -test("pointer args are valid JSON embedding the marker and the block ids", () => { - const a = msg("a", "m00003", "text"); - const [c, r] = pair("call1"); - const messages = [a, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - const out = relocateCompressCarriers(messages, state); - const callOut = out.find((m) => m.id === "call-call1")!; - const parsed = JSON.parse(callOut.text!) as { content: string }; - assert.ok(parsed.content.startsWith(RELOCATED_POINTER_MARKER)); - assert.ok(parsed.content.includes("b1")); - assert.ok(parsed.content.includes("do not re-compress")); -}); - -test("already relocated call (marker in args, carrier present) ⇒ same array reference", () => { - // Hosts may round-trip a view: carrier ids survive AND the args keep the - // pointer. Both probes must independently make the plan a no-op. - const a = msg("a", "m00003", "text"); - const [c, r] = pair("call1"); - const once = relocateCompressCarriers([a, c, r], stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - )); - // Carrier present ⇒ skip. - assert.equal(relocateCompressCarriers(once, stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - )), once); - // Marker only (carrier id stripped by a host round-trip) ⇒ skip. - const markerOnly: CoreMessage[] = once.filter((m) => m.id !== carrierId("call1")); - assert.equal(relocateCompressCarriers(markerOnly, stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - )), markerOnly); -}); - -test("idempotent: applying twice yields the identical array", () => { - const a = msg("a", "m00003", "text"); - const b = msg("b", "m00004", "text"); - const [c, r] = pair("call1"); - const messages = [a, b, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003", b: "m00004" }, - ); - const once = relocateCompressCarriers(messages, state); - const twice = relocateCompressCarriers(once, state); - assert.equal(twice, once); -}); - -test("duplicate tool-call cores for one call id ⇒ ambiguous, do not touch", () => { - const [c1, r1] = pair("call1"); - const c2 = msg("call-call1-again", null, "tool-call", { - toolName: "compress", - toolCallId: "call1", - }); - const messages = [msg("a", "m00003", "text"), c1, r1, c2]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - assert.equal(relocateCompressCarriers(messages, state), messages); -}); - -test("non-adjacent result (foreign core between call and result) ⇒ skip", () => { - const call = msg("mc", null, "tool-call", { - toolName: "compress", - toolCallId: "call1", - }); - const result = msg("mr", null, "tool-result", { toolCallId: "call1" }); - const foreign = msg("f", "m00003", "text"); - const messages = [msg("a", "m00004", "text"), call, foreign, result]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00004", f: "m00003" }, - ); - assert.equal(relocateCompressCarriers(messages, state), messages); -}); - -test("result with mismatched toolCallId ⇒ not a pair, skip", () => { - const call = msg("mc", null, "tool-call", { - toolName: "compress", - toolCallId: "call1", - }); - const result = msg("mr", null, "tool-result", { toolCallId: "other" }); - const messages = [msg("a", "m00003", "text"), call, result]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - assert.equal(relocateCompressCarriers(messages, state), messages); -}); - -test("BLOCKED_REF cores are not qualifying boundaries (durable-BLOCKED edge)", () => { - const protectedMsg = msg("prot", "BLOCKED", "text"); - const [c, r] = pair("call1"); - const messages = [protectedMsg, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { prot: "BLOCKED" }, - ); - // Nothing qualifies beyond the boundary ⇒ carrier appended at the tail; the - // pair stays in its slot untouched except the pointer rewrite. - const out = relocateCompressCarriers(messages, state); - assert.deepEqual(ids(out), ["prot", "call-call1", "res-call1", carrierId("call1")]); - assert.equal(out[0], protectedMsg); -}); - -test("multi-range batch shares one call id ⇒ boundary is max(endRef), pointer lists all blocks", () => { - const x = msg("x", "m00008", "text"); // inside later fold range - const y = msg("y", "m00011", "text"); // only survivor past both folds - const [c, r] = pair("callB"); - // Live views follow ingest (ref) order: x (8) precedes y (11). - const messages = [x, c, r, y]; - const state = stateWith( - [ - { blockId: "b1", compressCallId: "callB", startRef: "m00001", endRef: "m00005" }, - { blockId: "b2", compressCallId: "callB", startRef: "m00007", endRef: "m00010" }, - ], - { y: "m00011", x: "m00008" }, - ); - // Boundary = max(5,10)=10 ⇒ carrier before y (11 > 10), after x (8). - const out = relocateCompressCarriers(messages, state); - assert.deepEqual(ids(out), ["x", "call-callB", "res-callB", carrierId("callB"), "y"]); - const callOut = out.find((m) => m.id === "call-callB")!; - assert.ok(callOut.text!.includes("b1, b2")); - // Multi-block pair anchors the FIRST block for the #335 carrier protection. - const carrier = out.find((m) => m.id === carrierId("callB"))!; - assert.equal(carrier.summaryOfBlockId, "b1"); - assert.equal(isLiveCheckpointCarrier(carrier, state), true); -}); - -test("fold reaches the window end (#4732 regime) ⇒ carrier appended at tail", () => { - const [c, r] = pair("call1"); - const a = msg("a", "m00002", "text"); // only core; ref ≤ boundary - const messages = [a, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00002" }, - ); - const out = relocateCompressCarriers(messages, state); - assert.deepEqual(ids(out), ["a", "call-call1", "res-call1", carrierId("call1")]); -}); - -test("thinking clear: predicate unset ⇒ reasoning kept even with clearThinking set", () => { - const think = msg("t1", "m00009", "reasoning"); - const [c, r] = pair("call1"); - const a = msg("a", "m00010", "text"); - const messages = [think, c, r, a]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00008" }], - { a: "m00010" }, - ); - const out = relocateCompressCarriers(messages, state, { clearThinking: true }); - // Pair stays in its slot; thinking kept; carrier before the first ref>8 (a). - assert.deepEqual(ids(out), ["t1", "call-call1", "res-call1", carrierId("call1"), "a"]); -}); - -test("thinking clear: predicate-gated drop of the contiguous run before the call", () => { - const keep1 = msg("k1", "m00006", "reasoning"); - const drop1 = msg("d1", "m00007", "reasoning"); - const drop2 = msg("d2", "m00008", "reasoning"); - const [c, r] = pair("call1"); - const a = msg("a", "m00010", "text"); - const messages = [keep1, drop1, drop2, c, r, a]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00005" }], - { a: "m00010", k1: "m00006" }, - ); - // Predicate accepts d1/d2 but refuses k1 (e.g. signed) — scan continues PAST - // the refused core (documented behaviour). Dropped cores never qualify as - // boundaries; first qualifying survivor is k1 (6 > 5). - // v2: the pair KEEPS its slot (after k1 in the input); only the carrier - // moves (before k1, the first qualifying survivor). - const out = relocateCompressCarriers(messages, state, { - clearThinking: true, - reasoningDroppable: (m) => m.id !== "k1", - }); - assert.deepEqual(ids(out), [carrierId("call1"), "k1", "call-call1", "res-call1", "a"]); - assert.equal(out[1], keep1); -}); - -test("thinking clear: non-reasoning core stops the walk", () => { - const textBefore = msg("t0", "m00006", "text"); - const drop = msg("d1", "m00007", "reasoning"); - const [c, r] = pair("call1"); - const a = msg("a", "m00010", "text"); - const messages = [textBefore, drop, c, r, a]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00005" }], - { a: "m00010", t0: "m00004" }, - ); - const out = relocateCompressCarriers(messages, state, { - clearThinking: true, - reasoningDroppable: () => true, - }); - assert.deepEqual(ids(out), ["t0", "call-call1", "res-call1", carrierId("call1"), "a"]); -}); - -test("interleaved calls: carriers land at their boundaries regardless of pair slot order", () => { - const s1 = msg("s1", "m00009", "text"); - const s2 = msg("s2", "m00010", "text"); - const s3 = msg("s3", "m00011", "text"); - const [pa, ra] = pair("callA"); - const [pb, rb] = pair("callB"); - // B's pair sits EARLIER than A's although A's boundary (5) is smaller; both - // pairs keep their slots, only carriers are emitted (k=0 for both). - const messages = [s1, pb, rb, pa, ra, s2, s3]; - const state = stateWith( - [ - { blockId: "ba", compressCallId: "callA", startRef: "m00001", endRef: "m00005" }, - { blockId: "bb", compressCallId: "callB", startRef: "m00006", endRef: "m00008" }, - ], - { s1: "m00009", s2: "m00010", s3: "m00011" }, - ); - const out = relocateCompressCarriers(messages, state); - assert.deepEqual(ids(out), [ - carrierId("callB"), - carrierId("callA"), - "s1", - "call-callB", - "res-callB", - "call-callA", - "res-callA", - "s2", - "s3", - ]); -}); - -test("two distinct boundaries place carriers at different survivor slots", () => { - const t1 = msg("t1", "m00003", "text"); // > 2 (A's boundary) but ≤ 8 (B's) - const s2 = msg("s2", "m00009", "text"); // > 8 - const [pa, ra] = pair("callA"); - const [pb, rb] = pair("callB"); - const messages = [t1, s2, pa, ra, pb, rb]; - const state = stateWith( - [ - { blockId: "ba", compressCallId: "callA", startRef: "m00001", endRef: "m00002" }, - { blockId: "bb", compressCallId: "callB", startRef: "m00003", endRef: "m00008" }, - ], - { t1: "m00003", s2: "m00009" }, - ); - const out = relocateCompressCarriers(messages, state); - // A ⇒ before t1 (ref 3 > 2) ⇒ k=0; B ⇒ before s2 (first ref > 8) ⇒ k=1. - assert.deepEqual(ids(out), [ - carrierId("callA"), - "t1", - carrierId("callB"), - "s2", - "call-callA", - "res-callA", - "call-callB", - "res-callB", - ]); -}); - -test("refs are untouched by relocation", () => { - const a = msg("a", "m00003", "text"); - const [c, r] = pair("call1"); - const messages = [a, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - const before = structuredClone(state.messageRefs); - relocateCompressCarriers(messages, state); - assert.deepEqual(state.messageRefs, before); -}); - -test("isCompressTool override honours dialect names", () => { - const call = msg("mc", null, "tool-call", { - toolName: "acp_compress", - toolCallId: "callX", - }); - const result = msg("mr", null, "tool-result", { toolCallId: "callX" }); - const a = msg("a", "m00003", "text"); - const messages = [a, call, result]; - const state = stateWith( - [{ blockId: "bx", compressCallId: "callX", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - // Bare-name matching alone would NOT relocate this pair. - assert.equal(relocateCompressCarriers(messages, state), messages); - // With the host dialect predicate it does. - const out = relocateCompressCarriers(messages, state, { - isCompressTool: (n) => n === "acp_compress", - }); - assert.deepEqual(ids(out), [carrierId("callX"), "a", "mc", "mr"]); -}); - -test("plan and apply agree: no pair removals in v2, pointer+carrier well-formed", () => { - const a = msg("a", "m00003", "text"); - const [c, r] = pair("call1"); - const messages = [a, c, r]; - const state = stateWith( - [{ blockId: "b1", compressCallId: "call1", startRef: "m00001", endRef: "m00002" }], - { a: "m00003" }, - ); - const plan = planRelocateCompressCarriers(messages, state); - assert.equal(plan.changed, true); - // v2: the pair is NOT removed — only dropped reasoning ever lands here. - assert.deepEqual([...plan.removedInputIndices], []); - assert.equal(plan.moves.length, 1); - const move = plan.moves[0]!; - assert.equal(move.callIndex, 1); - assert.equal(move.resultIndex, 2); - assert.equal(move.insertAtOutput, 0); // before a (ref 3 > 2) - assert.equal(move.carrier.id, carrierId("call1")); - assert.ok(move.pointerText.includes(RELOCATED_POINTER_MARKER)); -}); diff --git a/package-lock.json b/package-lock.json index 206ead55ad..c5d1dc20ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ }, "kernel": { "name": "acp-kernel", - "version": "0.0.192", + "version": "0.0.193", "dev": true, "license": "MIT", "devDependencies": { diff --git a/src/compress-settings.ts b/src/compress-settings.ts index 6df979a182..c54c1a13c3 100644 --- a/src/compress-settings.ts +++ b/src/compress-settings.ts @@ -4,7 +4,6 @@ import { findRoute, resolveModelEntryOnRoute, type CompressSettings, type NamedP import { configDir } from "./paths.js"; import { log as loggerLog } from "./logger.js"; import { expandExternalSummaryChainTolerant, type ExternalSummarySettings } from "./external-summary-settings.js"; -import { resolveCompressAtFoldPoint, resolveCompressAtFoldPointClearThinking } from "./knobs.js"; import { shippedPacksSource } from "./chat-pack.js"; export type { CompressSettings } from "./config.js"; @@ -112,10 +111,6 @@ export function mergeCompress( outputSteering: outputSteeringLevels.length > 0 ? Object.assign({}, ...outputSteeringLevels) : undefined, priceProfile: priceProfileLevels.length > 0 ? Object.assign({}, ...priceProfileLevels) : undefined, reconcile: pick("reconcile"), - // Experimental fold-point carrier relocation (env-resolved later, in - // applyCompressSettings; here only the file tiers cascade). - compressAtFoldPoint: pick("compressAtFoldPoint"), - compressAtFoldPointClearThinking: pick("compressAtFoldPointClearThinking"), promptPack: pick("promptPack"), // Whole-chain replace (same semantics as `tiers`/`rules`): an // `externalSummary` object at a deeper level replaces the entire @@ -401,13 +396,6 @@ export function applyCompressSettings(base: Config, limit: number, s: CompressSe externalSummary: s.externalSummary !== undefined ? expandExternalSummaryChainTolerant(s.externalSummary, recipes) : (base as ResolvedKernelConfig).externalSummary, ...(absorb !== undefined ? { absorb } : {}), ...(ccr !== undefined ? { ccr } : {}), - // Fold-point carrier relocation pair: explicit pass-through so an unset - // flag never leaks a stale base value into the resolved config. - compressAtFoldPoint: resolveCompressAtFoldPoint(process.env, s.compressAtFoldPoint), - compressAtFoldPointClearThinking: resolveCompressAtFoldPointClearThinking( - process.env, - s.compressAtFoldPointClearThinking, - ), ...(s.rules !== undefined ? { rules: { enabled: s.rules === true } } : {}), }; return resolved; @@ -441,13 +429,6 @@ export function resolveRequestConfig( ): ResolvedKernelConfig { const compress = resolveCompress(routes, embeddedUrl, model, globalCompress); const limit = resolveContextLimitValue(compress.modelContextLimit, native ?? base.modelContextLimit); - // #5470 opt-in reachability: an env-only enablement of either fold-point - // switch is invisible to hasCompressSettings (env never joins the file - // cascade), so without these checks such a lane would hit the short-circuit - // below and the knob would silently die. Both-falsy keeps the original - // short-circuit byte-for-byte (zero behavior change when nothing is set). - const cafAt = resolveCompressAtFoldPoint(process.env, compress.compressAtFoldPoint); - const cafCt = resolveCompressAtFoldPointClearThinking(process.env, compress.compressAtFoldPointClearThinking); - if (!hasCompressSettings(compress) && limit === base.modelContextLimit && !cafAt && !cafCt) return base; + if (!hasCompressSettings(compress) && limit === base.modelContextLimit) return base; return applyCompressSettings(base, limit, compress, namedProviders ?? {}); } diff --git a/src/config.ts b/src/config.ts index bd3bc0b428..bbc14cf46f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -657,27 +657,6 @@ export type CompressSettings = { * Real edits never match (normalized text differs) and honestly * re-enter the wire unfolded, exactly as before. */ reconcile?: "off" | "warn" | "repair"; - /** EXPERIMENTAL view transform A (default OFF): move a still-visible - * compress call/result pair from its original tail-adjacent position to - * its own fold point, so resent history ends "[folded region] – [pair] – - * [protected messages]" instead of a tool exchange at the very end (which - * weak-attention models read as a fresh command). Purely view-side — refs - * and state are untouched; identical layout is recomputed every turn. - * Env BILI_COMPRESS_AT_FOLD_POINT wins over every level; - * applyCompressSettings resolves it per request. Maps to kernel - * Config.compressAtFoldPoint (acp-kernel >= 0.0.177; v2 semantics since - * 0.0.188 — summary text relocated to the fold point, pair kept in slot - * with pointer args). */ - compressAtFoldPoint?: boolean; - /** Second opt-in riding on compressAtFoldPoint (default OFF): also drop - * the contiguous run of UNSIGNED reasoning cores immediately preceding a - * relocated pair at the moment of repositioning (the turn's think about - * the fold itself). Both switches must be set for anything to be dropped, - * and the host additionally refuses any core carrying a wire signature or - * running under strict reasoning-echo (#1658/#684 doctrine). Env - * BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING wins over every level. - * Maps to kernel Config.compressAtFoldPointClearThinking. */ - compressAtFoldPointClearThinking?: boolean; }; /** #5079: governs every prompt_cache_key BILI ITSELF writes to an egress body * (chat-wire conversation-id stamping, Responses-wire identity injection). @@ -2572,8 +2551,6 @@ const COMPRESS_SETTING_FIELDS = new Set([ "reconcile", "streamSummary", "externalSummary", // #3635: summary-length caps — previously accepted nowhere, silently dropped. "maxSummaryLength", "minSummaryLength", - // Experimental fold-point carrier relocation switches (view-only). - "compressAtFoldPoint", "compressAtFoldPointClearThinking", ]); // Deduped per unique key set per process (same pattern as @@ -2584,7 +2561,11 @@ const seenUnknownTopLevelKeySignatures = new Set(); export function warnUnknownTopLevelKeys(obj: Record): void { // #3651: the removed top-level allowDshCompaction key gets its own deprecation // note (warnRemovedDshAllowKey) — exclude it here so it isn't named twice. - const unknown = Object.keys(obj).filter((key) => !KNOWN_TOP_LEVEL_KEYS.has(key) && key !== "allowDshCompaction"); + // #6132: the experimental fold-point relocation keys were REMOVED entirely + // (v1 #5471 + unreleased v2 #5949) — they parse nowhere and are inert at + // every level. Named here (not in the generic unknown-key warning) with + // the removal reason, like allowDshCompaction (#3651). + const unknown = Object.keys(obj).filter((key) => !KNOWN_TOP_LEVEL_KEYS.has(key) && key !== "allowDshCompaction" && !(INERT_REMOVED_COMPRESS_KEYS as readonly string[]).includes(key)); if (unknown.length === 0) return; const signature = [...unknown].sort().join("\u0000"); if (seenUnknownTopLevelKeySignatures.has(signature)) return; @@ -2812,7 +2793,15 @@ let seenInertStripImagesSignature = ""; // EVERY compress level it can appear (global / provider / model). (`absorb` was // removed by #3293 but REVIVED by #3402/#3422 — it parses again and is NOT inert.) // Dedup by dead-key path signature (#1815 style): re-warn when the set changes. -const INERT_REMOVED_COMPRESS_KEYS = ["imageCompression"] as const; +// #6132 + #3266: removed `compress.*` keys that may still sit in old configs. +// They parse nowhere and do nothing at any level — named once per distinct +// shape (deduped like warnUnknownTopLevelKeys) with the removal reason. +const INERT_REMOVED_COMPRESS_KEYS = ["imageCompression", "compressAtFoldPoint", "compressAtFoldPointClearThinking"] as const; +const INERT_REMOVED_COMPRESS_KEY_REASON: Record<(typeof INERT_REMOVED_COMPRESS_KEYS)[number], string> = { + imageCompression: "compress.imageCompression was removed and no longer does anything at any level (image pre-compression + image_full restore channel, #3266); images pass through byte-identical", + compressAtFoldPoint: "compress.compressAtFoldPoint (the experimental fold-point relocation, #5470) was removed in #6132 — the resent layout is the long-standing baseline and the flag no longer changes anything", + compressAtFoldPointClearThinking: "compress.compressAtFoldPointClearThinking (the companion opt-in of the removed fold-point relocation) was removed in #6132", +}; let seenInertRemovedCompressKeysSignature = ""; function warnInertRemovedCompressKeys(obj: Record): void { const hits: string[] = []; @@ -2822,6 +2811,9 @@ function warnInertRemovedCompressKeys(obj: Record): void { if ((block as Record)[key] !== undefined) hits.push(`${pathPrefix}.${key}`); } }; + for (const key of INERT_REMOVED_COMPRESS_KEYS) { + if (obj[key] !== undefined) hits.push(`top-level ${key}`); + } scanBlock(obj.compress, "compress"); const providers = obj.providers; if (providers && typeof providers === "object" && !Array.isArray(providers)) { @@ -2842,7 +2834,11 @@ function warnInertRemovedCompressKeys(obj: Record): void { const sig = hits.join(","); if (sig === "" || sig === seenInertRemovedCompressKeysSignature) return; seenInertRemovedCompressKeysSignature = sig; - loggerLog("warn", `[acp-config] ${hits.join(", ")} is INERT — compress.imageCompression was removed and no longer does anything at any level (image pre-compression + image_full restore channel, #3266). Remove it from the config; images pass through byte-identical.`); + const reasons = [...new Set(hits.map((h) => { + const key = h.split(".").pop(); + return INERT_REMOVED_COMPRESS_KEY_REASON[key as keyof typeof INERT_REMOVED_COMPRESS_KEY_REASON] ?? `${h} was removed and is ignored`; + }))]; + loggerLog("warn", `[acp-config] ${hits.join(", ")} is INERT — ${reasons.join("; ")}. Remove it from your config to silence this.`); } function warnInertResignPassthrough(obj: Record): void { @@ -3321,17 +3317,6 @@ export function parseCompressSettings(v: unknown): (CompressSettings & { injectT if (typeof obj.nudgeModelDecided !== "boolean") ok = false; else out.nudgeModelDecided = obj.nudgeModelDecided; } - // Experimental fold-point relocation pair (see CompressSettings docs): plain - // booleans; absent = off. Without this, file-tier/panel values are silently - // dropped by the parser while env resolution still honors them. - for (const key of ["compressAtFoldPoint", "compressAtFoldPointClearThinking"] as const) { - if (!(key in obj)) continue; - if (typeof obj[key] !== "boolean") { - ok = false; - continue; - } - out[key] = obj[key]; - } // #5683: unprompted/out-of-bounds compression policy gates — plain booleans, // off unless explicitly set (an explicit false at a deeper level disables). for (const key of ["requireNudge", "requireRecommendation"] as const) { diff --git a/src/knobs.ts b/src/knobs.ts index 8590c94762..c9649ceeeb 100644 --- a/src/knobs.ts +++ b/src/knobs.ts @@ -1068,38 +1068,3 @@ export function resolveStorageWalJournalSizeLimit(env: NodeJS.ProcessEnv = proce return WAL_JOURNAL_SIZE_LIMIT; } -/** EXPERIMENTAL fold-point carrier relocation switch (view transform A): when - * on, a still-visible compress call/result pair is moved from its original - * tail-adjacent position to its own fold point, so resent history ends in - * "[folded region] – [pair] – [protected messages]" instead of a tool exchange - * sitting at the very end (which weak-attention models read as a fresh - * command). Pure view-side transform — refs/state untouched, recomputed - * identically every turn; off = byte-identical to the pre-feature layout. - * OPT-IN (default OFF; verified on real models 2026-11-01 #5470, kept opt-in - * per owner decision). - * Env BILI_COMPRESS_AT_FOLD_POINT (0/false/off vs 1/true/on) wins over the - * config-file `compress.compressAtFoldPoint` boolean; absent everywhere = OFF. - * Garbage spellings fall through tiers. Pure: never throws. */ -export function resolveCompressAtFoldPoint(env: NodeJS.ProcessEnv = process.env, fileValue?: unknown): boolean { - const raw = env.BILI_COMPRESS_AT_FOLD_POINT?.trim().toLowerCase(); - if (raw === "1" || raw === "true" || raw === "on") return true; - if (raw === "0" || raw === "false" || raw === "off") return false; - return typeof fileValue === "boolean" ? fileValue : false; -} - -/** Second opt-in riding on BILI_COMPRESS_AT_FOLD_POINT: drop the contiguous - * run of unsigned reasoning cores immediately preceding a relocated pair at - * the moment of repositioning (the turn's "think" about the fold itself). - * Default OFF; both switches must be set for anything to be dropped, and the - * host additionally refuses any core carrying a wire signature or running - * under strict reasoning-echo (#1658/#684 doctrine — signed bytes are never - * rewritten or deleted). - * Env BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING wins over config-file - * `compress.compressAtFoldPointClearThinking`; absent = OFF. Pure: never - * throws. */ -export function resolveCompressAtFoldPointClearThinking(env: NodeJS.ProcessEnv = process.env, fileValue?: unknown): boolean { - const raw = env.BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING?.trim().toLowerCase(); - if (raw === "1" || raw === "true" || raw === "on") return true; - if (raw === "0" || raw === "false" || raw === "off") return false; - return typeof fileValue === "boolean" ? fileValue : false; -} diff --git a/src/plugin-render.ts b/src/plugin-render.ts index cbc118043e..636aca2043 100644 --- a/src/plugin-render.ts +++ b/src/plugin-render.ts @@ -10,10 +10,7 @@ // with NO resolvable row in the window skips its coverage (its rows stay // live-visible — the safe direction) instead of failing the whole render. import http from "node:http"; -import { relocateCompressCarriers } from "acp-kernel"; import { coreToAnthropic, type BiliMessage } from "acp-kernel/wire"; -import { loadConfigFile, isMcpBiliCompress } from "./config.js"; -import { resolveCompressAtFoldPoint } from "./knobs.js"; import { getStore } from "./persist.js"; import { peekSession } from "./session.js"; import { pluginAgentHeader } from "./plugin.js"; @@ -133,21 +130,6 @@ export async function handlePluginRender(req: http.IncomingMessage, res: http.Se merged.push(anchors[ai].msg); ai++; } - // Fold-point carrier relocation mirror (switch A): with the knob on, move - // still-visible compress pairs from their original slot to their fold point - // — the same view-only transform the kernel node applies on processTurn, - // so the rendered prefix and steady turns agree (v2: summary text relocated - // to the fold point, pair kept in slot with pointer args). S4 has no - // per-request strictness context, so no thinking clear here. - // KNOWN ASYMMETRY (v1, #5470 review): the knob is read from env + the - // global `compress` file section only, while steady turns resolve through - // the full cascade — a key set at a provider/model tier moves pairs on - // steady turns but not here. Deliberate for now (the feature ships off by - // default and global-tier is the common case); wire the resolved value - // through in a follow-up. - if (resolveCompressAtFoldPoint(process.env, loadConfigFile().compress?.compressAtFoldPoint)) { - merged = relocateCompressCarriers(merged as unknown as Parameters[0], session.state, { isCompressTool: isMcpBiliCompress }) as unknown as BiliMessage[]; - } const system = loadSystem(sessionId); // v1 renders the prefix WITHOUT cache-control marks: marks are per-turn diff --git a/src/server.ts b/src/server.ts index b8f5855b50..0f5aa38456 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2440,32 +2440,6 @@ export function applyReasoningDrop( return out; } -/** Predicate factory for the experimental fold-point carrier relocation - * "thinking clear" (kernel Config.reasoningDroppable): a reasoning core may - * be dropped at the moment its compress pair is repositioned ONLY when it - * carries no wire signature. Signed Anthropic thinking/redacted_thinking - * bytes are NEVER rewritten or deleted (#1658 doctrine). Returns undefined - * for sessions where the kernel must not drop anything at all: strict-echo - * upstreams (#684 — reasoning must round-trip with tool_calls) and views - * holding ANY signed thinking block (presence-based gate, same posture as - * planReasoningDrop), so a conservative misclassification can never break a - * signed session. Pure + cheap; install per request. */ -export function makeFoldPointReasoningDroppable( - msgs: BiliMessage[], - strictEcho: boolean, -): ((m: import("acp-kernel").CoreMessage) => boolean) | undefined { - if (strictEcho) return undefined; - if (msgs.some(hasSignedThinkingBlock)) return undefined; - return (m: import("acp-kernel").CoreMessage): boolean => { - const core = m as BiliMessage; - if (core.contentType === "reasoning") { - return !(typeof core.thinkingSignature === "string" && core.thinkingSignature.length > 0); - } - const raw = core.rawAnthropicBlock as { type?: unknown } | undefined; - return raw?.type !== "redacted_thinking"; - }; -} - /** [#651] Strip oversized reasoning from closed compress turns (see * src/reasoning-drop.ts) with an ops log line when anything was dropped. * Standalone form for callers holding an anchor-intact array (tests, ad-hoc diff --git a/src/server/prepare-anthropic.ts b/src/server/prepare-anthropic.ts index a76a48b3b5..f72055f492 100644 --- a/src/server/prepare-anthropic.ts +++ b/src/server/prepare-anthropic.ts @@ -7,7 +7,7 @@ import { captureThreadsTurn, estimateThreadsTokens, isThreadsBody, threadsReforg import type { CompressReasoningConfig } from "../reasoning-drop.js"; import type { ProxyOptions } from "../config.js"; import { pluginAgentHeader } from "../plugin.js"; -import { applyReasoningDrop, dedupeAdjacentOutputConfig, diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, makeFoldPointReasoningDroppable, planReasoningDrop, reapOrphansLogged, runNudgeDecision, stripKernelSummaries, warnAnthropicThinkingPairs, type Prepared } from "../server.js"; +import { applyReasoningDrop, dedupeAdjacentOutputConfig, diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, planReasoningDrop, reapOrphansLogged, runNudgeDecision, stripKernelSummaries, warnAnthropicThinkingPairs, type Prepared } from "../server.js"; import { recordNudgeObservation } from "../context-analysis.js"; import { buildDecisionPrompt, buildDirectiveText, consumeFallback, ladderMode, resolveDecisionRange, stripDecisionReasoningFields, type DecideConfig } from "../nudge-decide.js"; import { excludeDeadRefRanges } from "../acp-status.js"; @@ -305,10 +305,6 @@ export async function prepareAnthropic( // stamped per-request — strip `ccr` from the loop config when disarmed // (plugin mode / no tool channel) so placeholders never hit the wire. const loopConfig = ccrLoopConfig(session, { ...config, absorb: absorbActive ? absorbBlock : undefined }); - // Experimental fold-point "thinking clear" (default OFF): install the kernel drop predicate only when BOTH switches are set; strict-echo (#684) and signed-thinking views yield none at all (#1658). - if (loopConfig.compressAtFoldPointClearThinking) { - loopConfig.reasoningDroppable = makeFoldPointReasoningDroppable(renderMsgs, isStrictReasoningEcho(session, upstreamOrigin, modelIdOf(parsed))); - } // #2432/#2658: dsh desktop's native compaction can LAND without ever // transiting bili — its summarizer calls ctx.llm.stream() directly and // manual /compact / idle-session paths have no ALS attribution, so the diff --git a/src/server/prepare-google.ts b/src/server/prepare-google.ts index 6b2a1ba03a..1868694d82 100644 --- a/src/server/prepare-google.ts +++ b/src/server/prepare-google.ts @@ -5,9 +5,8 @@ import { coreToGoogle, googleToCore, normalizeGoogleUserPartOrder, type BiliMess import type { ProxyOptions } from "../config.js"; import { pluginAgentHeader } from "../plugin.js"; import { currentCalibrationFactor } from "../util.js"; -import { diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, makeFoldPointReasoningDroppable, reapOrphansLogged, resolveKnownOutputCeiling, runNudgeDecision, stripKernelSummaries, usageGradeInputBaseline, type Prepared } from "../server.js"; +import { diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, reapOrphansLogged, resolveKnownOutputCeiling, runNudgeDecision, stripKernelSummaries, usageGradeInputBaseline, type Prepared } from "../server.js"; import { recordNudgeObservation } from "../context-analysis.js"; -import { isStrictReasoningEcho } from "../strict-echo.js"; import { yieldToEventLoop } from "../yield-loop.js"; import { buildDecisionPrompt, buildDirectiveText, consumeFallback, ladderMode, resolveDecisionRange, stripDecisionReasoningFields, type DecideConfig } from "../nudge-decide.js"; import { excludeDeadRefRanges } from "../acp-status.js"; @@ -157,10 +156,6 @@ export async function prepareGoogle( // config stripping — only tool availability matters. const rulesActive = rulesEnabled(config) && shouldInject; const loopConfig = ccrLoopConfig(session, { ...config, absorb: absorbActive ? absorbBlock : undefined }); - // Experimental fold-point "thinking clear" (default OFF): install the kernel drop predicate only when BOTH switches are set; strict-echo (#684) and signed-thinking views yield none at all (#1658). - if (loopConfig.compressAtFoldPointClearThinking) { - loopConfig.reasoningDroppable = makeFoldPointReasoningDroppable(renderMsgs, isStrictReasoningEcho(session, upstreamOrigin, model)); - } // #2432/#2658: same post-landing recovery as the openai/responses/ // anthropic lanes — dsh desktop's native compaction can land without // transiting bili (direct-bypass / future gate mismatch); first notice = diff --git a/src/server/prepare-openai.ts b/src/server/prepare-openai.ts index 98cae44892..13e1133990 100644 --- a/src/server/prepare-openai.ts +++ b/src/server/prepare-openai.ts @@ -9,7 +9,7 @@ import { currentCalibrationFactor, hardenOpenaiAssistantContent, systemToUser } import { conversationHeaderSource, resolveRelayAffinityPck } from "../session-id.js"; import { MSG_IDS_BODY_FIELD, applyMsgIdPin, validateMsgIds } from "../msgid-pin.js"; import { pluginAgentHeader } from "../plugin.js"; -import { applyReasoningDrop, diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, planReasoningDrop, makeFoldPointReasoningDroppable, reapOrphansLogged, resolveKnownOutputCeiling, runNudgeDecision, stripKernelSummaries, warnDroppedOpenaiParts, warnReasoningPairs, type Prepared } from "../server.js"; +import { applyReasoningDrop, diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, planReasoningDrop, reapOrphansLogged, resolveKnownOutputCeiling, runNudgeDecision, stripKernelSummaries, warnDroppedOpenaiParts, warnReasoningPairs, type Prepared } from "../server.js"; import { recordNudgeObservation } from "../context-analysis.js"; import { yieldToEventLoop } from "../yield-loop.js"; import { buildDecisionPrompt, buildDirectiveText, consumeFallback, ladderMode, resolveDecisionRange, stripDecisionReasoningFields, type DecideConfig } from "../nudge-decide.js"; @@ -225,10 +225,6 @@ export async function prepareOpenai( const absorbActive = absorbBlock?.enabled === true && shouldInject; const rulesActive = rulesEnabled(config) && shouldInject; const loopConfig = ccrLoopConfig(session, { ...config, absorb: absorbActive ? absorbBlock : undefined }); - // Experimental fold-point "thinking clear" (default OFF): install the kernel drop predicate only when BOTH switches are set; strict-echo (#684) and signed-thinking views yield none at all (#1658). - if (loopConfig.compressAtFoldPointClearThinking) { - loopConfig.reasoningDroppable = makeFoldPointReasoningDroppable(renderMsgs, isStrictReasoningEcho(session, upstreamOrigin, modelIdOf(parsed))); - } // #2432: dsh desktop's native compaction can LAND without ever // transiting bili — its summarizer calls ctx.llm.stream() directly, // and manual /compact / idle-session paths have no ALS attribution so diff --git a/src/server/prepare-responses.ts b/src/server/prepare-responses.ts index 0453487697..31ae4937d9 100644 --- a/src/server/prepare-responses.ts +++ b/src/server/prepare-responses.ts @@ -6,7 +6,7 @@ import type { CompressReasoningConfig } from "../reasoning-drop.js"; import { resolveAbsorbSettings } from "../compress-settings.js"; import { effectivePromptCacheRouting, resolveCompressProtocol, type CompressSettings, type ProxyOptions } from "../config.js"; import { currentCalibrationFactor, strippedResponseIdWarning } from "../util.js"; -import { applyReasoningDrop, diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, makeFoldPointReasoningDroppable, planReasoningDrop, reapOrphansLogged, resolveKnownOutputCeiling, runNudgeDecision, stripKernelSummaries, usageGradeInputBaseline, warnResponsesReasoningPairs, type Prepared } from "../server.js"; +import { applyReasoningDrop, diagNudge, diagTagSummary, deriveTitle, effectiveTokenCount, imageBillingFor, imageReserveFor, imageTokenCapFor, isAutoInjectedNotification, planReasoningDrop, reapOrphansLogged, resolveKnownOutputCeiling, runNudgeDecision, stripKernelSummaries, usageGradeInputBaseline, warnResponsesReasoningPairs, type Prepared } from "../server.js"; import { collectSeparatorManifest, repairResponsesAssistantOrdering, type SeparatorSlot } from "./responses-ordering.js"; import { recordNudgeObservation } from "../context-analysis.js"; import { yieldToEventLoop } from "../yield-loop.js"; @@ -261,10 +261,6 @@ export async function prepareResponses( const absorbActive = absorbBlock?.enabled === true && shouldInject && !isCompactionTrigger && !responsesTextProtocol; const rulesActive = rulesEnabled(config) && shouldInject && !isCompactionTrigger && !responsesTextProtocol; const loopConfig = ccrLoopConfig(session, { ...config, absorb: absorbActive ? absorbBlock : undefined }); - // Experimental fold-point "thinking clear" (default OFF): install the kernel drop predicate only when BOTH switches are set; strict-echo (#684) and signed-thinking views yield none at all (#1658). - if (loopConfig.compressAtFoldPointClearThinking) { - loopConfig.reasoningDroppable = makeFoldPointReasoningDroppable(renderMsgs, isStrictReasoningEcho(session, upstreamOrigin, modelIdOf(parsed))); - } // #2372/#2432: LOCAL auto-compaction — the client compacts on its own // and the call never transits the proxy; the first the proxy hears of // it is this request replaying [compaction summary, retained tail…]. diff --git a/src/server/relay.ts b/src/server/relay.ts index cd3687db46..99c6cb5667 100644 --- a/src/server/relay.ts +++ b/src/server/relay.ts @@ -8,7 +8,7 @@ import { repairResponsesAssistantOrdering, reanchorSeparatorManifest, separatorM import http from "node:http"; import fs from "node:fs"; import path from "node:path"; -import { type CompressionCore, type Config, type CoreMessage, defaultCountTokens, defaultPrompts, relocateCompressCarriers, resolveOutputSteeringConfig } from "acp-kernel"; +import { type CompressionCore, type Config, type CoreMessage, defaultCountTokens, defaultPrompts, resolveOutputSteeringConfig } from "acp-kernel"; import { type BiliMessage } from "acp-kernel/wire"; import { absorbToolName, applyAbsorbView } from "../absorb.js"; import { APIG_RESIGN_SCHEME, resignApig } from "../apig-resign.js"; @@ -75,8 +75,7 @@ import { buildForwardHeaders, connectionNamedHeaders, RESPONSE_ONLY_STRIP_HEADER import { bodyDumpEnabled, logDumpFailure } from "./observability.js"; import { effectiveAbsorbBlock } from "./prepare-responses.js"; import { awaitDrain, bufferToStream, dumpStreamToFile, pipeThrough, readStreamToBuffer } from "./stream-io.js"; -import { buildForwardTarget, makeFoldPointReasoningDroppable, measuredBreakdownClause, measuredOutbound, outboundPayloadBreakdown, registerRequestAbort, beginStreamKeepalive, effectiveTokenCount, googlePathKind, imageBillingFor, imageTokenCapFor, inboundResignCredential, outboundContextEstimates, resolveUpstream, stripKernelSummaries, usageGradeInputBaseline, DOOMED_PROBE_ABSOLUTE_BYTES, DOOMED_PROBE_BYTES_PER_TOKEN, type Prepared } from "../server.js"; -import { isStrictReasoningEcho, modelIdOf } from "../strict-echo.js"; +import { buildForwardTarget, measuredBreakdownClause, measuredOutbound, outboundPayloadBreakdown, registerRequestAbort, beginStreamKeepalive, effectiveTokenCount, googlePathKind, imageBillingFor, imageTokenCapFor, inboundResignCredential, outboundContextEstimates, resolveUpstream, stripKernelSummaries, usageGradeInputBaseline, DOOMED_PROBE_ABSOLUTE_BYTES, DOOMED_PROBE_BYTES_PER_TOKEN, type Prepared } from "../server.js"; /** Infer the wire protocol from the request path for compat-role rewrites on * requests the pipeline did not prepare (passthrough). Mirrors the path @@ -2611,12 +2610,6 @@ async function forwardCaptured( const absorbBlock = effectiveAbsorbBlock(prepared.pluginMode === true, config, opts.compress.absorb); const absorbActive = absorbBlock?.enabled === true && opts.compress.injectTool && !textProtocol; const loopConfig = ccrLoopConfig(prepared.session, { ...config, absorb: absorbActive ? absorbBlock : undefined }); - // Experimental fold-point "thinking clear" (default OFF): install the - // kernel drop predicate only when BOTH switches are set; strict-echo - // and signed-thinking views yield none at all (#684/#1658). - if (loopConfig.compressAtFoldPointClearThinking) { - loopConfig.reasoningDroppable = makeFoldPointReasoningDroppable(prepared.originalMessages as BiliMessage[], isStrictReasoningEcho(prepared.session, targetOrigin, modelIdOf({ model: prepared.model }))); - } // Cache-seam: the compress<->absorb join must be byte-identical to // the steady prepare* paths. Every steady wire joins the absorb // section with a plain "\n\n" (sysParts.join / injectSystem parts @@ -2697,20 +2690,7 @@ async function forwardCaptured( // against them drops the only cross-turn summary carrier and breaks the byte-prefix // at the fold anchor (post-fold cache floor reset to the stable head). const outBase = [...stripKernelSummaries(viewed as BiliMessage[], turn.state), ...(records as BiliMessage[])] as BiliMessage[]; - // Fold-point summary relocation round-2 mirror (switch A): with - // the switch on, each still-visible compress pair keeps its - // chronological slot but its SUMMARY TEXT is re-emitted at its - // fold point (call args become a pointer — v2 semantics). - // Idempotent when processTurn's node already relocated them - // (view-only + deterministic transform). - const out = loopConfig.compressAtFoldPoint - ? relocateCompressCarriers(outBase as unknown as CoreMessage[], turn.state, { - isCompressTool: loopConfig.isCompressTool, - ...(loopConfig.compressAtFoldPointClearThinking && loopConfig.reasoningDroppable - ? { clearThinking: true, reasoningDroppable: loopConfig.reasoningDroppable } - : {}), - }) as BiliMessage[] - : outBase; + const out = outBase; // [#1592] Mirror the steady path's per-wire post-processing EXACTLY: // the reasoning drop each prepare* applied, and — only on the // Responses wire — the run-ordering repair (#564). Applying the diff --git a/src/stream.ts b/src/stream.ts index f1e8e7d874..502acda0af 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -1072,21 +1072,6 @@ export function applyRanges(parsed: ReturnType, ctx: } let { ranges, diagnostics } = parsed; if (ranges.length === 0) { - // compressAtFoldPoint v2 pointer echo (kind=already-folded-echo): the - // call's content was the relocation ack text of an earlier fold — the - // model copied the most recent compress call's rewritten args as its - // template (field report on #5949's v2: four identical - // content-not-array receipts in 25 minutes, each receipt feeding the - // next retry). Nothing is wrong with the format and nothing is - // foldable: ack a no-op instead of lecturing. NOT routed through - // recordCompressFailure (no repeat-guard counter for the model to - // misread as "the user asked N times"), but loop-tracked so a runaway - // echo burst still collapses to the #2146 short refusal. - if (diagnostics.kind === "already-folded-echo") { - const loopGuard = trackLoop ? noteCompressLoopFailure(ctx, "already-folded-echo") : ""; - ctx.log("[acp-proxy: compress call echoed a fold-point relocation pointer (compressAtFoldPoint v2) — acked as no-op]"); - return compressResult(`[bili: nothing was compressed — the text you sent is the relocation pointer of an earlier, already-completed fold (its summary lives at its fold point in the history). Do not re-send it. To fold NEW consumed ranges, issue one compress call whose content holds fresh 'mNNNNN–mNNNNN optional topic' header lines with their summaries.]${loopGuard}`, "refused", 0, "already-folded-echo"); - } ctx.log("[acp-proxy: compress call had no valid ranges; nothing compressed.]"); const rawReasons = diagnostics.invalidReasons ?? []; for (const reason of rawReasons) ctx.log(`[acp-proxy: rejected entry: ${reason}]`); diff --git a/src/web/client.ts b/src/web/client.ts index e8dacb587a..672fecdc0a 100644 --- a/src/web/client.ts +++ b/src/web/client.ts @@ -4004,7 +4004,6 @@ if (d.keepalive) kv(parts, t("det.keepalive"), d.keepalive.tierLabel + " · " + tsRow.inp.checked = cp ? cp.messageTimestamps !== false : true; const ab = (cp && cp.absorb && typeof cp.absorb === "object" && !Array.isArray(cp.absorb)) ? cp.absorb : null; abRow.inp.checked = ab ? ab.enabled === true : false; - cafRow.inp.checked = cp ? cp.compressAtFoldPoint === true : false; rnRow.inp.checked = cp ? cp.requireNudge === true : false; rrRow.inp.checked = cp ? cp.requireRecommendation === true : false; const pv = (cp && typeof cp.promptPack === "string") ? cp.promptPack : "default"; @@ -4231,26 +4230,6 @@ if (d.keepalive) kv(parts, t("det.keepalive"), d.keepalive.tierLabel + " · " + } })); void tsRow.ctl; - // #5470 fold-point carrier relocation — default OFF (opt-in); uncheck - // deletes the key so the file stays clean (mirrors the absorb row). - const cafRow = row("quick-compress-at-fold-point", t("cfg.q_cafp")); - qCtrls.push(cafRow.inp); - cafRow.inp.addEventListener("change", () => commit((d) => { - if (cafRow.inp.checked) { - if (!compressOf(d)) d.compress = {}; - d.compress.compressAtFoldPoint = true; - } else { - const c = compressOf(d); - if (c) { - delete c.compressAtFoldPoint; - if (Object.keys(c).length === 0) delete d.compress; - } - } - })); - const cafHint = document.createElement("span"); - cafHint.className = "dim small"; - cafHint.textContent = t("cfg.q_cafp_hint"); - cafRow.ctl.appendChild(cafHint); const pw = document.createElement("div"); pw.style.cssText = "display:flex;gap:12px;align-items:center;flex-wrap:wrap"; const plab = document.createElement("label"); diff --git a/src/web/i18n.ts b/src/web/i18n.ts index 1fc7729aaa..300b125925 100644 --- a/src/web/i18n.ts +++ b/src/web/i18n.ts @@ -585,8 +585,6 @@ export const MESSAGES: Record> = { "cfg.q_dsh_ctxtab_label": "DSH「上下文」标签页名称(dsh.contextTabLabel)——留空用内置名「上下文」,可改名避免与其他插件重名", "cfg.q_absorb": "工具结果即时蒸馏(absorb)——大工具结果到达时由模型即时蒸馏,原文隐藏到下一轮折叠前(默认关)", "cfg.q_absorb_hint": "默认只吸含图结果(#3422);mediaOnly: false 可恢复吸全文。子项(minToolTokens、contextThresholdPct、excludeTools)请在下方 JSON 编辑框写", - "cfg.q_cafp": "压缩摘要前置到折叠点(compressAtFoldPoint,默认关)——把每次压缩产生的摘要文本以纯文本形式放到它描述的折叠点处,原 compress 调用对留在原时间位置、参数替换成指针,不再是可执行的完整压缩命令", - "cfg.q_cafp_hint": "针对 qwen3-27b 这类注意力机制有问题的小模型很有用:避免历史中部一整墙“完整形状”的 compress 交互被误读成继续压缩的示范/新命令;取消勾选 = 回到旧版布局(字节级一致)。可选的“清未签名思考”开关(clearThinking)只能走 env / JSON", "cfg.q_require_nudge": "禁止未提示压缩(compress.requireNudge)——只在 nudge 信用存活时(最近一次真正注入的提醒起 20 个模型轮内,新提醒刷新、未提示轮衰减、成功折叠不消耗)允许模型调用 compress,其余拒绝并回错误回执(默认关)", "cfg.q_require_nudge_hint": "针对会自行乱压的模型(如 qwen 27b)。bili 自己的溢出兜底折叠不受影响;也可只对特定 provider/model 开启(JSON 编辑框)", "cfg.q_require_rec": "禁止越界压缩(compress.requireRecommendation)——只允许压缩 bili 当前推荐的范围,越界即拒(默认关)", @@ -1340,8 +1338,6 @@ export const MESSAGES: Record> = { "cfg.q_dsh_ctxtab_label": "Label of the DSH Context tab (dsh.contextTabLabel) — empty keeps the built-in name \"Context\"; rename to avoid clashing with another plugin's tab", "cfg.q_absorb": "Instant tool-result absorption (absorb) — large tool results are distilled by the model on arrival; originals hidden until the next fold round (default off)", "cfg.q_absorb_hint": "by default only image-bearing results are absorbed (#3422); set mediaOnly: false to absorb full text. Sub-fields (minToolTokens, contextThresholdPct, excludeTools) go in the JSON editor below", - "cfg.q_cafp": "Summary text relocated to the fold point (compressAtFoldPoint, default off) — each fold's summary text is re-emitted as plain text at the fold point it describes, while the compress call/result pair keeps its chronological slot with its args replaced by a pointer (no longer a fully-shaped compress command)", - "cfg.q_cafp_hint": "very useful for small models with attention issues such as qwen3-27b: stops a mid-history wall of fully-shaped compress interactions from reading as an invitation to keep compressing; uncheck = byte-identical legacy layout. The optional clear-thinking companion (clearThinking) stays env/JSON-only", "cfg.q_require_nudge": "Forbid unprompted compression (compress.requireNudge) — compress may only be called while a nudge credit is live (within 20 model rounds of the last actually-injected nudge: refreshed by new nudges, decayed per un-nudged round, NOT consumed by successful folds); anything else is refused with an error receipt (default off)", "cfg.q_require_nudge_hint": "for models that fold on their own (e.g. qwen 27b). bili's own overflow backstop folds are unaffected; can also be scoped per provider/model in the JSON editor", "cfg.q_require_rec": "Forbid out-of-bounds compression (compress.requireRecommendation) — only ranges bili currently recommends may be folded; anything else is refused (default off)", diff --git a/tests/compress-at-fold-point.test.ts b/tests/compress-at-fold-point.test.ts deleted file mode 100644 index 221573c211..0000000000 --- a/tests/compress-at-fold-point.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Experimental fold-point carrier relocation (switch A + switch B): host-side -// coverage for the two knobs (env > file > ON-by-default), config parsing, -// settings mapping into the kernel Config (incl. the resolveRequestConfig -// short-circuit gate), and the signed/strict-echo drop-predicate -// doctrine (#1658/#684). Kernel pipeline behavior is covered by -// kernel/tests/relocate-compress.test.ts. -import test from "node:test"; -import assert from "node:assert/strict"; -import { defaultConfig, type CoreMessage } from "acp-kernel"; -import { applyCompressSettings, resolveRequestConfig } from "../src/compress-settings.ts"; -import { parseCompressSettings } from "../src/config.ts"; -import { resolveCompressAtFoldPoint, resolveCompressAtFoldPointClearThinking } from "../src/knobs.ts"; -import { makeFoldPointReasoningDroppable } from "../src/server.ts"; - -function withEnv(key: string, value: string | undefined, fn: () => void): void { - const prev = process.env[key]; - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - try { - fn(); - } finally { - if (prev === undefined) delete process.env[key]; - else process.env[key] = prev; - } -} - -test("resolveCompressAtFoldPoint: env tier wins, garbage falls through, default off", () => { - assert.equal(resolveCompressAtFoldPoint({ BILI_COMPRESS_AT_FOLD_POINT: "1" } as NodeJS.ProcessEnv), true); - assert.equal(resolveCompressAtFoldPoint({ BILI_COMPRESS_AT_FOLD_POINT: "ON" } as NodeJS.ProcessEnv), true); - assert.equal(resolveCompressAtFoldPoint({ BILI_COMPRESS_AT_FOLD_POINT: " 0 " } as NodeJS.ProcessEnv, true), false, "env 0 beats file true"); - assert.equal(resolveCompressAtFoldPoint({ BILI_COMPRESS_AT_FOLD_POINT: "bogus" } as NodeJS.ProcessEnv, true), true, "garbage env falls to file"); - assert.equal(resolveCompressAtFoldPoint({ BILI_COMPRESS_AT_FOLD_POINT: "" } as NodeJS.ProcessEnv, false), false); - assert.equal(resolveCompressAtFoldPoint({} as NodeJS.ProcessEnv), false, "opt-in default off (#5470)"); - assert.equal(resolveCompressAtFoldPoint({ BILI_COMPRESS_AT_FOLD_POINT: "off" } as NodeJS.ProcessEnv), false, "explicit env off"); - assert.equal(resolveCompressAtFoldPoint({} as NodeJS.ProcessEnv, true), true, "file bool honored"); - assert.equal(resolveCompressAtFoldPoint({} as NodeJS.ProcessEnv, false), false, "file false honored"); - assert.equal(resolveCompressAtFoldPoint({} as NodeJS.ProcessEnv, "yes"), false, "non-bool file falls to default off"); -}); - -test("resolveCompressAtFoldPointClearThinking: same tiers, default off", () => { - assert.equal(resolveCompressAtFoldPointClearThinking({ BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING: "on" } as NodeJS.ProcessEnv), true); - assert.equal(resolveCompressAtFoldPointClearThinking({ BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING: "off" } as NodeJS.ProcessEnv, true), false); - assert.equal(resolveCompressAtFoldPointClearThinking({ BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING: "?" } as NodeJS.ProcessEnv, true), true); - assert.equal(resolveCompressAtFoldPointClearThinking({} as NodeJS.ProcessEnv), false); - assert.equal(resolveCompressAtFoldPointClearThinking({} as NodeJS.ProcessEnv, true), true); -}); - -test("parseCompressSettings: accepts both fold-point booleans, rejects non-boolean", () => { - const ok = parseCompressSettings({ compressAtFoldPoint: true, compressAtFoldPointClearThinking: true }); - assert.equal(ok?.compressAtFoldPoint, true); - assert.equal(ok?.compressAtFoldPointClearThinking, true); - assert.equal(parseCompressSettings({ compressAtFoldPoint: false })?.compressAtFoldPoint, false, "explicit false kept"); - assert.equal(parseCompressSettings({ compressAtFoldPoint: "yes" }), undefined, "non-boolean rejected"); - assert.equal(parseCompressSettings({ compressAtFoldPointClearThinking: 1 }), undefined, "non-boolean rejected"); -}); - -test("applyCompressSettings: maps file flags and lets env override", () => { - const base = defaultConfig(200000); - withEnv("BILI_COMPRESS_AT_FOLD_POINT", undefined, () => { - withEnv("BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING", undefined, () => { - const on = applyCompressSettings(base, 200000, { compressAtFoldPoint: true, compressAtFoldPointClearThinking: true }); - assert.equal(on.compressAtFoldPoint, true); - assert.equal(on.compressAtFoldPointClearThinking, true); - const unset = applyCompressSettings(base, 200000, {}); - assert.equal(unset.compressAtFoldPoint, false, "unset file flag resolves to opt-in default off (explicit pass-through)"); - assert.equal(unset.compressAtFoldPointClearThinking, false, "clear-thinking stays opt-in"); - }); - }); - withEnv("BILI_COMPRESS_AT_FOLD_POINT", "0", () => { - const out = applyCompressSettings(base, 200000, { compressAtFoldPoint: true }); - assert.equal(out.compressAtFoldPoint, false, "env 0 beats file true"); - }); - withEnv("BILI_COMPRESS_AT_FOLD_POINT", "1", () => { - const out = applyCompressSettings(base, 200000, { compressAtFoldPoint: false }); - assert.equal(out.compressAtFoldPoint, true, "env 1 beats file false"); - }); -}); - -test("resolveRequestConfig: env-only fold-point opt-in reaches lanes with zero compress settings", () => { - const base = defaultConfig(200000); - const model = "model-x"; - // nothing set anywhere -> legacy short-circuit preserved byte-for-byte - withEnv("BILI_COMPRESS_AT_FOLD_POINT", undefined, () => { - withEnv("BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING", undefined, () => { - const resolved = resolveRequestConfig(base, {}, undefined, model, 200000); - assert.equal(resolved, base, "both-falsy knobs keep returning base untouched"); - }); - }); - // env-only switch-A must still be stamped (env never joins the file - // cascade, so hasCompressSettings alone cannot see it) - withEnv("BILI_COMPRESS_AT_FOLD_POINT", "on", () => { - const resolved = resolveRequestConfig(base, {}, undefined, model, 200000); - assert.notEqual(resolved, base, "env-only opt-in must not die in the short-circuit"); - assert.equal(resolved.compressAtFoldPoint, true); - assert.equal(resolved.compressAtFoldPointClearThinking, false); - }); - // env-only switch-B alone must also force the mapping to run - withEnv("BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING", "on", () => { - const resolved = resolveRequestConfig(base, {}, undefined, model, 200000); - assert.notEqual(resolved, base); - assert.equal(resolved.compressAtFoldPointClearThinking, true); - }); -}); - -test("makeFoldPointReasoningDroppable: strict-echo / signed views never get a predicate", () => { - const unsignedReasoning: CoreMessage = { id: "r1", role: "assistant", contentType: "reasoning", text: "thinking..." }; - const signedReasoning: CoreMessage & { thinkingSignature?: string } = { id: "r2", role: "assistant", contentType: "reasoning", text: "signed", thinkingSignature: "sig123" }; - const userMsg: CoreMessage = { id: "u1", role: "user", contentType: "text", text: "hi" }; - // strictEcho ⇒ undefined (no predicate at all, #684) - assert.equal(makeFoldPointReasoningDroppable([unsignedReasoning] as never[], true), undefined); - // any signed block in view ⇒ undefined (#1658 presence gate) - assert.equal(makeFoldPointReasoningDroppable([unsignedReasoning, signedReasoning] as never[], false), undefined); - // clean view ⇒ predicate that drops only unsigned reasoning cores - const pred = makeFoldPointReasoningDroppable([userMsg, unsignedReasoning] as never[], false); - assert(typeof pred === "function"); - assert.equal(pred!(unsignedReasoning), true, "unsigned reasoning droppable"); - assert.equal(pred!({ ...signedReasoning }), false, "signed reasoning protected"); - assert.equal(pred!(userMsg), true, "non-reasoning core is not a think run member (predicate consulted per core by kernel walk)"); -}); diff --git a/tests/compress-reject-reasons.test.ts b/tests/compress-reject-reasons.test.ts index 0b7aec63d0..7498164628 100644 --- a/tests/compress-reject-reasons.test.ts +++ b/tests/compress-reject-reasons.test.ts @@ -116,24 +116,9 @@ test("line-form string content with refs headers is not redirected to flat salva }); // --------------------------------------------------------------------------- -// #5949 field: v2 pointer echo ack + repeat-parse-failure receipt collapse +// #5949 field: repeat-parse-failure receipt collapse // --------------------------------------------------------------------------- -test("compress call echoing the v2 relocation pointer is acked as a no-op, not lectured", () => { - const echo = { content: '{"content": "[bili] compression succeeded — summary relocated to its fold point above (block(s) b49); these ranges are already folded — do not re-compress them."}' }; - const ctx = makeCtx(); - const first = applyRanges(parseCompressInput(echo), ctx).text; - assert.ok(first.startsWith("[bili: nothing was compressed"), `echo acked, got: ${first}`); - assert.ok(!first.includes("Compression FAILED"), "echo is not a format failure"); - // No repeat-guard counter: the counter text was misread by the model as - // "the user asked the same thing N times". - assert.ok(!first.includes("Repeat-failure guard"), "no repeat-failure guard on echo"); - // Repeats stay counter-free acks (no failure streak accumulates). - const second = applyRanges(parseCompressInput(echo), ctx).text; - assert.ok(second.startsWith("[bili: nothing was compressed"), `repeat echo acked, got: ${second}`); - assert.ok(!second.includes("Repeat-failure guard"), "no counter on repeat echo"); -}); - test("identical parse failures collapse to a one-line receipt from the 2nd on", () => { const bad = { content: "just prose with no refs at all" }; const ctx = makeCtx(); diff --git a/tests/e2e/fake-upstream-chat.mjs b/tests/e2e/fake-upstream-chat.mjs index b56707a7ad..bd21393036 100644 --- a/tests/e2e/fake-upstream-chat.mjs +++ b/tests/e2e/fake-upstream-chat.mjs @@ -41,6 +41,14 @@ function flatContent(c) { return String(c ?? ""); } +/** Ref of the message's own ACP render tag (the tag wraps the first line of + * the rendered message). Null when the message carries no tag (untagged side + * lanes, or the first-request stamp race #1243). */ +function acpRefOf(msg) { + const m = flatContent(msg?.content ?? "").match(/\x3cacp\s+[^>]*?\x3e(m\d+)\x3c\/acp\x3e/); + return m ? m[1] : null; +} + function acpRefs(body, role) { const raw = role === undefined ? JSON.stringify(body.messages ?? []) @@ -216,11 +224,28 @@ function answerFor(convKey, firstUserText, body) { if (isTitleCall) return { content: "e2e-title", queueIdx: -1, title: true }; const lastUserText = users.length > 0 ? flatContent(users[users.length - 1].content) : firstUserText; let sourceText = lastUserText; + let sourceRef = users.length > 0 ? acpRefOf(users[users.length - 1]) : null; if (parseDirectives(lastUserText).length === 0 && users.length > 1) { const firstText = flatContent(users[0].content); - if (firstText !== lastUserText && parseDirectives(firstText).length > 0) sourceText = firstText; + if (firstText !== lastUserText && parseDirectives(firstText).length > 0) { + sourceText = firstText; + sourceRef = acpRefOf(users[0]); + } } - const queueKey = `${convKey}|${sourceText.slice(0, 64)}`; + // Queue identity = conv + source message REF + tag-stripped body head. + // The ACP render tag around the body is VOLATILE by design: the post-fold + // ledger alignment (#5608-D — refs renumber onto store-row seats at the + // next successful fold) and the #5608-C time-attribute high-water gate both + // rewrite the tag bytes of messages that already went out the wire. Keying + // on the RAW text (old behavior) therefore split one scripted turn into + // two queues the instant a fold landed — the tag prefix inside the 64-char + // slice changed (time= dropped, ref reminted) and the directive queue + // replayed from scratch (#2791 "compress called twice" flake). The ref + // names the message OCCURRENCE (stable for the queue's lifetime; a remint + // voids the script anyway — the message's bytes are in a fold block), and + // stripAcps pins the body itself. Two distinct prompts stay distinct via + // their refs even when their bodies are byte-identical. + const queueKey = `${convKey}|${sourceRef ?? "noref"}|${stripAcps(sourceText).slice(0, 64)}`; if (!convs.has(queueKey)) { convs.set(queueKey, { directives: parseDirectives(sourceText), idx: 0 }); } diff --git a/tests/removed-compress-keys-inert.test.ts b/tests/removed-compress-keys-inert.test.ts index 111be4b0a8..f26db43a46 100644 --- a/tests/removed-compress-keys-inert.test.ts +++ b/tests/removed-compress-keys-inert.test.ts @@ -32,7 +32,7 @@ function withConfigFile(body: string, fn: () => void): void { // Warnings for the removed-key family only (other INERT emitters exist too). const familyWarns = (captured: Captured[]) => - captured.filter((e) => e.level === "warn" && e.msg.includes("is INERT") && /absorb|imageCompression/.test(e.msg)); + captured.filter((e) => e.level === "warn" && e.msg.includes("is INERT") && /absorb|imageCompression|compressAtFoldPoint/.test(e.msg)); // `absorb` was removed by #3293 but REVIVED by #3402/#3422 — it parses again // and is NOT inert; only imageCompression (#3266) stays in the removed family. @@ -73,6 +73,31 @@ test("#3344: removed key at provider and model levels is named by full path; pro stop(); }); +test("#6132: removed fold-point keys warn INERT once at every level (top-level, root, provider, model) and are ignored", () => { + const { captured, stop } = captureLogs(); + withConfigFile( + `{"compressAtFoldPoint":true,"compress":{"compressAtFoldPoint":true,"compressAtFoldPointClearThinking":true},"providers":{"https://u.example":{"compress":{"compressAtFoldPoint":true},"models":{"gpt-x":{"compress":{"compressAtFoldPointClearThinking":true}}}}}}`, + () => { + const cfg = loadConfigFile(); + const cs = parseCompressSettings(cfg.compress ?? {}); + assert.equal((cs as Record).compressAtFoldPoint, undefined); + assert.equal((cs as Record).compressAtFoldPointClearThinking, undefined); + const warns = familyWarns(captured); + assert.equal(warns.length, 1, `expected exactly one family INERT warning, got: ${warns.map((w) => w.msg).join(" | ")}`); + const msg = warns[0]!.msg; + assert.match(msg, /top-level compressAtFoldPoint/); + assert.match(msg, /compress\.compressAtFoldPoint/); + assert.match(msg, /compress\.compressAtFoldPointClearThinking/); + assert.match(msg, /providers\[\"https:\/\/u\.example\"\]\.models\[\"gpt-x\"\]\.compress\.compressAtFoldPointClearThinking/); + assert.match(msg, /#6132/); + // Second load of the SAME content: deduped (one notice per process). + loadConfigFile(); + assert.equal(familyWarns(captured).length, 1); + }, + ); + stop(); +}); + test("#3344: configs without the removed keys stay silent", () => { const { captured, stop } = captureLogs(); withConfigFile(`{"compress":{"ccr":{"enabled":true}}}`, () => { diff --git a/website/articles.json b/website/articles.json index 6acd6ba5d5..4a68fcf9cf 100644 --- a/website/articles.json +++ b/website/articles.json @@ -9,11 +9,6 @@ "file": "website/blog/posts/2026-11-01_frequent-compression.md", "zh_file": "website/blog/posts/2026-11-01_frequent-compression.zh.md", "slug": "frequent-compression" - }, - { - "file": "website/blog/posts/2026-11-02_compress-at-fold-point.md", - "zh_file": "website/blog/posts/2026-11-02_compress-at-fold-point.zh.md", - "slug": "compress-at-fold-point" } ] } diff --git a/website/blog/compress-at-fold-point.html b/website/blog/compress-at-fold-point.html deleted file mode 100644 index a2bb694506..0000000000 --- a/website/blog/compress-at-fold-point.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - -What to do when your model keeps treating compressed old content as fresh user instructions? · billion-context 博客 Blog - - - - - -
-

What to do when your model keeps treating compressed old content as fresh user instructions?

- -

Some models, especially ones with weak attention like qwen3.8 27b, always rework things once compression kicks in. The reason: the content of the compressed summary gets treated as a user instruction. There is now a pretty good solution to this problem. After a compress call, we swap the positions of compress and the recent protected user messages, so on the next request the model treats compress as historical messages, avoiding attention drift. This parameter is an experimental feature, off by default, and can be turned on by ticking "fold replacement in place (compressAtFoldPoint)" on the web config page (or JSON compress.compressAtFoldPoint: true / env var BILI_COMPRESS_AT_FOLD_POINT=1).

-

← Blog / 博客

-
- - diff --git a/website/blog/compress-at-fold-point.zh.html b/website/blog/compress-at-fold-point.zh.html deleted file mode 100644 index 4b37f22c97..0000000000 --- a/website/blog/compress-at-fold-point.zh.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - -模型总是把压缩老内容当成用户新指令咋办? · billion-context 博客 Blog - - - - - -
-

模型总是把压缩老内容当成用户新指令咋办?

- -

一些模型尤其是注意力比如弱的模型比如qwen3.8 27b,只要一压缩,总是返工.原因是压缩摘要的内容当作用户的指令.这个问题现在有了一个比较好的解决办法.就是调用compress 后,我们把compress和最近几条用户保护消息交换位置, 这样下一次请求模型就以为compress是历史消息,从而避免注意力漂移.参数属于实验性功能,默认关闭,在 web 配置页勾选「压缩折叠是否原位置替换」(或 JSON compress.compressAtFoldPoint: true / 环境变量 BILI_COMPRESS_AT_FOLD_POINT=1)打开.

-

← Blog / 博客

-
- - diff --git a/website/blog/index.html b/website/blog/index.html index d2a2b2a70d..ab9f50b3ac 100644 --- a/website/blog/index.html +++ b/website/blog/index.html @@ -21,11 +21,6 @@

博客 Blog

发布说明、设计笔记和事故复盘——都写自仓库里真实发生的事情。

  • - -

    What to do when your model keeps treating compressed old content as fresh user instructions?  /  模型总是把压缩老内容当成用户新指令咋办?

    -

    Weak-attention models read compressed summaries as fresh instructions — what to do?弱注意力模型把压缩摘要当成新指令,怎么办?

    -
  • -
  • What to do when billion-context compresses frequently?  /  billion-context频繁压缩该怎么办?

    Is it broken?是不是坏了

    diff --git a/website/blog/posts/2026-11-02_compress-at-fold-point.md b/website/blog/posts/2026-11-02_compress-at-fold-point.md deleted file mode 100644 index 9a97042d14..0000000000 --- a/website/blog/posts/2026-11-02_compress-at-fold-point.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "What to do when your model keeps treating compressed old content as fresh user instructions?" -date: 2026-11-02 -excerpt: "Weak-attention models read compressed summaries as fresh instructions — what to do?" -excerpt_zh: "弱注意力模型把压缩摘要当成新指令,怎么办?" ---- - -Some models, especially ones with weak attention like qwen3.8 27b, always rework things once compression kicks in. The reason: the content of the compressed summary gets treated as a user instruction. There is now a pretty good solution to this problem. After a compress call, we swap the positions of compress and the recent protected user messages, so on the next request the model treats compress as historical messages, avoiding attention drift. This parameter is an experimental feature, off by default, and can be turned on by ticking "fold replacement in place (compressAtFoldPoint)" on the web config page (or JSON `compress.compressAtFoldPoint: true` / env var `BILI_COMPRESS_AT_FOLD_POINT=1`). diff --git a/website/blog/posts/2026-11-02_compress-at-fold-point.zh.md b/website/blog/posts/2026-11-02_compress-at-fold-point.zh.md deleted file mode 100644 index 640f5edfe5..0000000000 --- a/website/blog/posts/2026-11-02_compress-at-fold-point.zh.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "模型总是把压缩老内容当成用户新指令咋办?" -date: 2026-11-02 -excerpt: "弱注意力模型把压缩摘要当成新指令,怎么办?" -excerpt_zh: "弱注意力模型把压缩摘要当成新指令,怎么办?" ---- - -一些模型尤其是注意力比如弱的模型比如qwen3.8 27b,只要一压缩,总是返工.原因是压缩摘要的内容当作用户的指令.这个问题现在有了一个比较好的解决办法.就是调用compress 后,我们把compress和最近几条用户保护消息交换位置, 这样下一次请求模型就以为compress是历史消息,从而避免注意力漂移.参数属于实验性功能,默认关闭,在 web 配置页勾选「压缩折叠是否原位置替换」(或 JSON `compress.compressAtFoldPoint: true` / 环境变量 `BILI_COMPRESS_AT_FOLD_POINT=1`)打开. diff --git a/website/config-reference/compress.yaml b/website/config-reference/compress.yaml index e1ebcc29c3..058f36d66b 100644 --- a/website/config-reference/compress.yaml +++ b/website/config-reference/compress.yaml @@ -205,28 +205,6 @@ entries: desc_zh: "客户端在轮次之间回退或改写历史时校准已折叠状态。" site_en: "\"off\" | \"warn\" | \"repair\" (default repair): reconcile folded state when the client rewinds or rewrites history between turns." site_zh: "\"off\" | \"warn\" | \"repair\"(默认 repair):客户端在轮次之间回退或改写历史时校准已折叠状态。" - - key: compress.compressAtFoldPoint - type: boolean - default: "false" - env: BILI_COMPRESS_AT_FOLD_POINT - group: compress - id: cw.cafp - render: compress-row - desc_en: "EXPERIMENTAL, default off: move a still-visible compress call/result pair from its original (tail-adjacent) slot to its own fold point in the re-sent view, i.e. [folded region] – [pair] – [protected messages], so weak-attention models stop reading a trailing tool exchange as a fresh command. View-only transform: refs and state untouched, recomputed identically on every turn; off = byte-identical layout." - desc_zh: "实验性,默认关闭:把仍可见的压缩调用/结果对从原位置(贴近尾部)移到它自己的折叠点,即 [已折叠区]–[调用对]–[保护消息],避免弱注意力模型把历史末尾的工具交换当成新命令。纯视图变换:refs 与状态不变、每轮重算结果一致;关闭时布局字节级不变。" - site_en: "EXPERIMENTAL boolean (default false): relocate a visible compress call/result pair to its fold point in the outbound view so a trailing tool exchange is not mistaken for a new user command. View-only and deterministic per turn; off = byte-identical to the default layout." - site_zh: "实验性布尔开关(默认 false):把可见的压缩调用/结果对移到出站视图的折叠点处,避免历史末尾的工具交换被模型误读为新命令。纯视图变换、每轮确定性重算;关闭 = 与原布局字节级一致。" - - key: compress.compressAtFoldPointClearThinking - type: boolean - default: "false" - env: BILI_COMPRESS_AT_FOLD_POINT_CLEAR_THINKING - group: compress - id: cw.cafct - render: compress-row - desc_en: "Second opt-in riding on compress.compressAtFoldPoint: when the pair is repositioned, also drop the contiguous run of unsigned reasoning cores immediately preceding it (the model's thinking about this very fold), so the moved pair starts clean. Inert unless the main switch is on; signed thinking blocks and strict reasoning-echo sessions are never touched (#1658/#684)." - desc_zh: "依附于 compress.compressAtFoldPoint 的第二层开关:调用对被挪位的一瞬间,同时丢掉紧邻其前的未签名思考段(模型对本次折叠本身的思考),让挪位后的开头更干净。主开关关闭时无效;带签名的思考块与严格推理回显会话绝不受影响(#1658/#684)。" - site_en: "EXPERIMENTAL boolean (default false): together with compress.compressAtFoldPoint, clear the unsigned thinking run immediately before a relocated compress pair at the moment of repositioning. Never touches signed thinking or strict reasoning-echo sessions (#1658/#684); inert while the main switch is off." - site_zh: "实验性布尔开关(默认 false):配合 compress.compressAtFoldPoint,在调用对被挪位的同时清掉其紧邻之前的未签名思考段。绝不触碰带签名的思考块与严格推理回显会话(#1658/#684);主开关关闭时无效。" - key: compress.promptPack type: "string (builtin: \"default\", \"lean\")" default: "builtin \"default\"" diff --git a/website/docs/index.html b/website/docs/index.html index 8c40176001..3e4bf64991 100644 --- a/website/docs/index.html +++ b/website/docs/index.html @@ -201,8 +201,6 @@

    compress — every knob

    maxSummaryLengthnumberHard cap, in characters, on one generated summary (kernel default 20000): longer replies are rejected whole by the kernel check or the external-summary executor. External summarizers receive this exact cap in their instructions (#3635); 0 disables the cap. minSummaryLengthnumberFloor, in characters, for generated summaries (kernel default 50): a single range must meet it on its own; a multi-range batch passes when its COMBINED summaries reach it — one lazy paragraph no longer sinks the whole batch (#5894). 0 disables the floor. External summarizers remain held to the per-summary floor (#3635). reconcile"off" | "warn" | "repair""off" | "warn" | "repair" (default repair): reconcile folded state when the client rewinds or rewrites history between turns. - compressAtFoldPointbooleanEXPERIMENTAL boolean (default false): relocate a visible compress call/result pair to its fold point in the outbound view so a trailing tool exchange is not mistaken for a new user command. View-only and deterministic per turn; off = byte-identical to the default layout. - compressAtFoldPointClearThinkingbooleanEXPERIMENTAL boolean (default false): together with compress.compressAtFoldPoint, clear the unsigned thinking run immediately before a relocated compress pair at the moment of repositioning. Never touches signed thinking or strict reasoning-echo sessions (#1658/#684); inert while the main switch is off. promptPackstring (builtin: "default", "lean")Compression prompt pack, resolved project pack → user pack → builtin. Builtins: "default" and "lean" (one-line tool descriptions). Not gated by acknowledgePromptsRisk. tiersbooleanOn by default: tiered T1→T3 distillation spreads folding cost across generations. protectedToolsstring[]Hard exclusion across all history: results of listed tools never fold (use with care). diff --git a/website/main.js b/website/main.js index 261d840532..1c30e0b268 100644 --- a/website/main.js +++ b/website/main.js @@ -81,8 +81,6 @@ "cw.msl": "Hard cap, in characters, on one generated summary (kernel default 20000): longer replies are rejected whole by the kernel check or the external-summary executor. External summarizers receive this exact cap in their instructions (#3635); 0 disables the cap.", "cw.mnl": "Floor, in characters, for generated summaries (kernel default 50): a single range must meet it on its own; a multi-range batch passes when its COMBINED summaries reach it — one lazy paragraph no longer sinks the whole batch (#5894). 0 disables the floor. External summarizers remain held to the per-summary floor (#3635).", "cw.rec": "\"off\" | \"warn\" | \"repair\" (default repair): reconcile folded state when the client rewinds or rewrites history between turns.", - "cw.cafp": "EXPERIMENTAL boolean (default false): relocate a visible compress call/result pair to its fold point in the outbound view so a trailing tool exchange is not mistaken for a new user command. View-only and deterministic per turn; off = byte-identical to the default layout.", - "cw.cafct": "EXPERIMENTAL boolean (default false): together with compress.compressAtFoldPoint, clear the unsigned thinking run immediately before a relocated compress pair at the moment of repositioning. Never touches signed thinking or strict reasoning-echo sessions (#1658/#684); inert while the main switch is off.", "cw.ppk": "Compression prompt pack, resolved project pack → user pack → builtin. Builtins: \"default\" and \"lean\" (one-line tool descriptions). Not gated by acknowledgePromptsRisk.", "cw.tiers": "On by default: tiered T1→T3 distillation spreads folding cost across generations.", "ct.pt": "Hard exclusion across all history: results of listed tools never fold (use with care).", @@ -340,8 +338,6 @@ "cw.msl": "单条生成摘要的硬上限,单位为字符(内核默认 20000):超长的回复由内核检查或外部摘要执行器整段拒绝。外部摘要模型的指令中会写明这个确切上限(#3635);0 禁用上限。", "cw.mnl": "生成摘要的下限,单位为字符(内核默认 50):单段压缩按自身判定;批量压缩按各段合计判定——一段偷懒不再拖垮整批(#5894)。0 禁用下限。外部摘要模型仍受逐段下限约束(#3635)。", "cw.rec": "\"off\" | \"warn\" | \"repair\"(默认 repair):客户端在轮次之间回退或改写历史时校准已折叠状态。", - "cw.cafp": "实验性布尔开关(默认 false):把可见的压缩调用/结果对移到出站视图的折叠点处,避免历史末尾的工具交换被模型误读为新命令。纯视图变换、每轮确定性重算;关闭 = 与原布局字节级一致。", - "cw.cafct": "实验性布尔开关(默认 false):配合 compress.compressAtFoldPoint,在调用对被挪位的同时清掉其紧邻之前的未签名思考段。绝不触碰带签名的思考块与严格推理回显会话(#1658/#684);主开关关闭时无效。", "cw.ppk": "压缩提示词包,按 项目包 → 用户包 → 内置 解析。内置:\"default\" 与 \"lean\"(单行工具描述)。不受 acknowledgePromptsRisk 门控。", "cw.tiers": "默认开:T1→T3 分级蒸馏,把折叠成本摊到多代。", "ct.pt": "全历史硬排除:列出工具的结果永不折叠(慎用)。",