opencode-acp 1.12.10 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/NOTICE +29 -0
  2. package/README.md +123 -37
  3. package/README.zh-CN.md +100 -20
  4. package/dist/index.js +917 -54
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  7. package/dist/lib/compress/quality-gate/algorithms/index.d.ts +3 -0
  8. package/dist/lib/compress/quality-gate/algorithms/index.d.ts.map +1 -0
  9. package/dist/lib/compress/quality-gate/evaluate.d.ts +8 -0
  10. package/dist/lib/compress/quality-gate/evaluate.d.ts.map +1 -0
  11. package/dist/lib/compress/quality-gate/index.d.ts +5 -0
  12. package/dist/lib/compress/quality-gate/index.d.ts.map +1 -0
  13. package/dist/lib/compress/quality-gate/registry.d.ts +6 -0
  14. package/dist/lib/compress/quality-gate/registry.d.ts.map +1 -0
  15. package/dist/lib/compress/quality-gate/types.d.ts +51 -0
  16. package/dist/lib/compress/quality-gate/types.d.ts.map +1 -0
  17. package/dist/lib/config-validation.d.ts.map +1 -1
  18. package/dist/lib/config.d.ts +9 -0
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/messages/inject/policy/index.d.ts +5 -0
  21. package/dist/lib/messages/inject/policy/index.d.ts.map +1 -0
  22. package/dist/lib/messages/inject/policy/registry.d.ts +8 -0
  23. package/dist/lib/messages/inject/policy/registry.d.ts.map +1 -0
  24. package/dist/lib/messages/inject/policy/types.d.ts +2 -0
  25. package/dist/lib/messages/inject/policy/types.d.ts.map +1 -0
  26. package/dist/lib/messages/inject/utils.d.ts +0 -9
  27. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  28. package/dist/lib/messages/utils.d.ts.map +1 -1
  29. package/dist/lib/ui/notification.d.ts.map +1 -1
  30. package/package.json +5 -2
  31. package/dist/lib/prompts/compression-rules.d.ts +0 -20
  32. package/dist/lib/prompts/compression-rules.d.ts.map +0 -1
package/NOTICE ADDED
@@ -0,0 +1,29 @@
1
+ opencode-acp — NOTICE
2
+
3
+ Copyright (c) 2026 ranxianglei
4
+ Licensed under AGPL-3.0-or-later (see ./LICENSE).
5
+
6
+ This distribution bundles portions of context-compress-algorithms
7
+ (https://github.com/ranxianglei/context-compress-algorithms), which is
8
+ originally published under the MIT License. The MIT-licensed source retains
9
+ its MIT status when consumed directly from that project; the bytes inlined
10
+ into this AGPL distribution become part of the combined AGPL work.
11
+
12
+ For the standalone MIT-licensed version, install
13
+ `context-compress-algorithms` directly.
14
+
15
+ MIT License (context-compress-algorithms):
16
+
17
+ Copyright (c) 2026 ranxianglei
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a
20
+ copy of this software and associated documentation files (the "Software"),
21
+ to deal in the Software without restriction, including without limitation
22
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
23
+ and/or sell copies of the Software, and to permit persons to whom the
24
+ Software is furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included
27
+ in all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
  <strong>Active Context Pruning</strong> for <a href="https://opencode.ai">OpenCode</a>
5
5
  <br />
6
6
  The model decides <em>when</em> and <em>what</em> to compress — not a hard limit.
7
+ <br />
8
+ <strong>200K tokens is enough.</strong>
7
9
  </p>
8
10
 
9
11
  ---
@@ -28,10 +30,14 @@ is, to date, the best context-management implementation on the market.
28
30
 
29
31
  This brings two concrete effects:
30
32
 
31
- - **It saves about two-thirds of tokens.** A model with a 1,000,000-token context
32
- window effectively runs in the **200,000–300,000 token range**.
33
- - **It supports ultra-long sessions without losing key content** **500M-token-level
34
- cumulative context, 100,000 messages per session**.
33
+ - **200K tokens is enough.** Across 30,000+ API calls in 50 real engineering
34
+ sessions, **97% of requests stayed under 200K tokens** — p90 at 150K, p95 at
35
+ 180K. Every API call re-bills the full context, so keeping context low directly
36
+ reduces cost — even with a 90%+ prompt-cache hit rate, the non-cached portion
37
+ is billed at full price.
38
+ - **It supports ultra-long sessions without losing key content** — observed at
39
+ **3,300+ messages and 300M+ cumulative tokens** per session; architecturally
40
+ supports up to **100,000 messages** (5-digit message-ID space).
35
41
 
36
42
  ---
37
43
 
@@ -39,22 +45,23 @@ This brings two concrete effects:
39
45
 
40
46
  Real engineering context, in practice.
41
47
 
42
- **Supports 500M-token-level cumulative context, with p95 context around 30% and
43
- an average prompt-cache hit ratio above 85%.** (That average not per-session
44
- is explained in [Impact on Prompt Caching](#impact-on-prompt-caching), where it
45
- turns out to save far more tokens than traditional compression.)
46
-
47
- | | Session 1 | Session 2 |
48
- | -------------------------- | ----------- | ----------- |
49
- | **Messages** | 3,024 | 2,028 |
50
- | **Total tokens processed** | 582 M | 463 M |
51
- | **Prompt-cache hit ratio** | 86.2% | 89.0% |
52
- | **Context p50 (median)** | 1.2 K (<1%) | 1.8 K (<1%) |
53
- | **Context p75** | 2.8 K | 3.5 K |
54
- | **Context p90** | 108 K (11%) | 58 K (6%) |
55
- | **Context p95** | 251 K (25%) | 335 K (34%) |
56
- | **Context p99** | 425 K (43%) | 442 K (44%) |
57
- | **Peak** | 488 K (49%) | 769 K (77%) |
48
+ **Across 6 active engineering sessions (11,000+ API calls), context p90 stays
49
+ at 150K–190K (15–19%), p95 at 160K–210K (16–21%) of the 1M window with an
50
+ aggregate prompt-cache hit ratio of 91%.** (Why aggregate — not per-session
51
+ matters is explained in [Impact on Prompt Caching](#impact-on-prompt-caching),
52
+ where it turns out to save far more tokens than traditional compression.)
53
+
54
+ | Session | Duration | Messages | API calls | Cumulative | Cache hit | Context p50 | Context p90 | Context p95 |
55
+ | --------- | ----------- | -------- | --------- | ---------- | --------- | ----------- | ----------- | ----------- |
56
+ | 0b89319b | 230h (9.5d) | 3,344 | 2,796 | 339M | 93% | 108K (11%) | 167K (17%) | 210K (21%) |
57
+ | 0a3be0cd | 130h (5.4d) | 3,183 | 2,499 | 276M | 91% | 104K (10%) | 145K (15%) | 153K (15%) |
58
+ | 0b2cd5a7 | 131h (5.4d) | 2,560 | 2,181 | 314M | 91% | 142K (14%) | 191K (19%) | 197K (20%) |
59
+ | 08f2d501 | 37h (1.5d) | 1,985 | 1,888 | 196M | 95% | 100K (10%) | 156K (16%) | 168K (17%) |
60
+ | 1410c791† | 865h (36d) | 1,279 | 1,100 | 218M | 87% | 132K (13%) | 407K (41%) | 427K (43%) |
61
+ | 096cf8c4 | 72h (3d) | 1,041 | 918 | 91M | 89% | 92K (9%) | 148K (15%) | 161K (16%) |
62
+
63
+ Bug-testing session; p95 is abnormally high. Excluding it, p95 stays ≤ 210K
64
+ across all other sessions.
58
65
 
59
66
  (Context percentages are of the 1M window.)
60
67
 
@@ -81,9 +88,10 @@ Or add to your opencode config:
81
88
  ## How It Works
82
89
 
83
90
  ACP hands the context-compression tool directly to the model. The model is
84
- **100% responsible** for context compression. The model's available tools are
85
- mainly: **compress** and **decompress**. A hardcoded 100% GC fallback acts as
86
- a safety net when the context window is completely full.
91
+ **100% responsible** for context compression. The model's primary tools are
92
+ **compress** and **decompress**, supported by **acp_status** (context monitoring)
93
+ and **search_context** (search compressed content). A hardcoded 100% GC fallback
94
+ acts as a safety net when the context window is completely full.
87
95
 
88
96
  ### Lifecycle
89
97
 
@@ -122,27 +130,46 @@ interfere with the model's self-attention, short blocks lead the model to compre
122
130
  some content first, handle the urgent matter, then decompress what it needs in
123
131
  later work.
124
132
 
125
- ### Deletion strategy
133
+ ### GC safety net
134
+
135
+ When context reaches 100%, the system automatically truncates old-gen block summaries to prevent overflow. This is a last-resort safety net and does not interfere with the model's normal compress/decompress operations.
136
+
137
+ ### Quality gate (non-blocking, off by default)
126
138
 
127
- To handle the accumulation of many small historical blocks, the new version adds
128
- a deletion strategy. The model decides whether to delete. **Once deleted, content
129
- is irrecoverable.** This replaces the original forced GC, so that forced garbage
130
- collection no longer deletes things the model considers important.
139
+ After each `compress` call, ACP can run a pluggable quality gate to detect summaries that catastrophically lost content (e.g., a 5K-token range compressed to a 147-char summary with none of the technical keywords). Failures only emit `logger.warn` — they never reject the compression (the result is already committed to state and visible to the model).
140
+
141
+ The default algorithm (`rouge-recall-v1`) is a two-layer gate calibrated against 6,913 real-world blocks:
142
+
143
+ - **L1 (length floor)**: Catches catastrophic retention failures — summaries shorter than 200 chars OR with <1% retention vs. the original. 100% recall, 0% FPR.
144
+ - **L2 (content coverage)**: Only runs on blocks that pass L1. Flags when **both** ROUGE-1 F1 < 0.05 **and** top-20 keyword recall < 0.20 (AND-combine keeps FPR at ~6.6%).
145
+
146
+ The interface is pluggable: future algorithms (e.g., LLM-as-judge via external API) can be registered through `registerQualityGate()` without touching pipeline wiring. Tokenizer uses hand-rolled word-level tokenization (English keywords + Chinese unigrams/bigrams) — not ACP's BPE tokenizer, which is too coarse for ROUGE-style matching.
147
+
148
+ Off by default for one release of burn-in. To enable:
149
+
150
+ ```jsonc
151
+ {
152
+ "qualityGate": {
153
+ "enabled": true,
154
+ "algorithm": "rouge-recall-v1",
155
+ },
156
+ }
157
+ ```
131
158
 
132
159
  ---
133
160
 
134
161
  ## Impact on Prompt Caching
135
162
 
136
163
  Historically, ACP has fixed many of the low-cache-hit-rate problems caused by
137
- DCP. The overall cache hit rate is now **~87%**.
164
+ DCP. The overall cache hit rate is now **~91%**.
138
165
 
139
166
  Compared to traditional compression — which only compresses at 80–90% and, once it
140
167
  compresses, forces 100% of the context to re-hit — ACP's hit rate is effectively
141
168
  higher.
142
169
 
143
- Additionally, ACP keeps total context around **~30% most of the time**, versus the
144
- traditional **50–80%**. So total token savings are far higher than traditional
145
- compression.
170
+ Additionally, ACP keeps total context around **~10–15% most of the time** (p50
171
+ 100K, p90 150K of the 1M window), versus the traditional **50–80%**. So total
172
+ token savings are far higher than traditional compression.
146
173
 
147
174
  **Conclusion:** ACP simultaneously raises the overall cache hit rate **and**
148
175
  ensures key context information is not lost.
@@ -208,9 +235,9 @@ Each level overrides the previous, so project settings take priority over global
208
235
  // Enable debug logging to ~/.config/opencode/logs/acp/
209
236
  "debug": false,
210
237
  // Notification display: "off", "minimal", or "detailed"
211
- "pruneNotification": "detailed",
212
- // Notification type: "chat" (in-conversation) or "toast" (system toast)
213
- "pruneNotificationType": "chat",
238
+ "pruneNotification": "off",
239
+ // Notification type: "chat" (deprecated, falls back to toast) or "toast" (system toast)
240
+ "pruneNotificationType": "toast",
214
241
  // Slash commands configuration
215
242
  "commands": {
216
243
  "enabled": true,
@@ -319,6 +346,28 @@ Each level overrides the previous, so project settings take priority over global
319
346
  // run major GC when context usage exceeds this (hardcoded, not configurable)
320
347
  "majorGcThresholdPercent": "100%",
321
348
  },
349
+ // Post-compression quality gate (non-blocking; off by default)
350
+ "qualityGate": {
351
+ // Master switch. When false, no evaluation runs.
352
+ "enabled": false,
353
+ // Algorithm name. Pluggable — future algorithms (including external
354
+ // API judges) can be registered without changing pipeline wiring.
355
+ "algorithm": "rouge-recall-v1",
356
+ // Per-algorithm config
357
+ "algorithms": {
358
+ "rouge-recall-v1": {
359
+ // Hard floor on summary length (chars). Below this → L1 fails.
360
+ "layer1MinChars": 200,
361
+ // Min retention = summaryLen / (compressedTokens*4) * 100.
362
+ // Catches catastrophic retention failures (<1%) with 0% FPR.
363
+ "layer1MinRetentionPct": 1.0,
364
+ // L2 fails (combined with top20Recall via AND) when below this.
365
+ "layer2MaxRougeF1": 0.05,
366
+ // L2 fails (combined with rougeF1 via AND) when below this.
367
+ "layer2MaxTop20Recall": 0.20,
368
+ },
369
+ },
370
+ },
322
371
  }
323
372
  ```
324
373
 
@@ -350,7 +399,7 @@ By default, these tools are always protected from pruning:
350
399
 
351
400
  The `protectedTools` arrays in `commands` and `strategies` add to this default list.
352
401
 
353
- For the `compress` tool, `compress.protectedTools` ensures specific tool outputs are **hard-excluded** from compression ranges (v1.10.0+). When the model compresses a range that includes a protected tool message, that message survives intact in visible context — only the surrounding non-protected messages are compressed. By default `compress.protectedTools` includes `task`, `skill`, `todowrite`, `todoread`, and `decompress`.
402
+ For the `compress` tool, `compress.protectedTools` ensures specific tool outputs are **hard-excluded** from compression ranges (v1.10.0+). When the model compresses a range that includes a protected tool message, that message survives intact in visible context — only the surrounding non-protected messages are compressed. By default `compress.protectedTools` includes only `skill` this is sufficient in practice, as skill outputs are the one tool type whose content must never be lost to compression.
354
403
 
355
404
  ---
356
405
 
@@ -422,6 +471,41 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
422
471
 
423
472
  ## Changelog
424
473
 
474
+ ### v1.13.2 — Preserve Last User Msg + Config Defaults Tuning (PR #169)
475
+
476
+ **Problem**: Two issues remained after v1.13.1's notification freeze fix. (1) When the model compressed a range that covered all visible user messages, the next API call had zero user-role messages — zhipuai-lb rejected this with the same HTTP 400 code 1214 (`isRetryable: false`), freezing the session. This was the second path to the same freeze that v1.13.1's empty-notification fix addressed. (2) The default `pruneNotification: "detailed"` fired a toast on every compress call (10–30 per session is typical), which was over-intrusive for a routine background operation. Additionally, `compress.maxSummaryLengthHard: 10000` rejected ~25% of information-dense useful summaries in real sessions.
477
+
478
+ **Fix**: (1) `lib/messages/prune.ts` — `filterCompressedRanges` rewritten as a two-pass filter: pass 1 computes survivors, pass 2 builds the result; if no user-role message would survive, the most recent pruned user message is restored to keep the API request shape valid. The restore is transform-time only — `byMessageId` still records the message as compressed. (2) `lib/config.ts` — default `pruneNotification` changed `"detailed"` → `"off"`; compression events still log to `~/.config/opencode/logs/acp/` via a new always-log path in `lib/ui/notification.ts` (lossless observability without UI noise). (3) `lib/config.ts` — default `compress.maxSummaryLengthHard` raised `10000` → `20000` (aligns with observed good-summary lengths). (4) `dcp.schema.json` — 4 stale defaults synced. Files: `lib/messages/prune.ts`, `lib/config.ts`, `lib/ui/notification.ts`, `dcp.schema.json`, `README.md`. Tests: 803 pass (5 new regression tests for the preserve-last-user fix).
479
+
480
+ ### v1.13.1 — cc-alg Extraction + Compress Notification Freeze Fix (PRs #167, #168)
481
+
482
+ **Problem (compress notification freeze, #167)**: After every successful `compress` tool call, ACP injected a user-role notification message with a single `ignored: true` text part. opencode strips `ignored` parts before sending to the LLM, leaving an empty user message. The provider (zhipuai-lb / glm-5.2) rejects this with HTTP 400 code 1214 (`"messages 参数非法"`), `isRetryable: false` — opencode does not retry, and the session freezes until external recovery. 113 total occurrences across active sessions (8 in a single 3,156-message session).
483
+
484
+ **Fix (compress notification freeze, #167)**: (1) `lib/ui/notification.ts:280-298` — `sendCompressNotification` now always uses `client.tui.showToast`; the prior `chat` branch (which called `sendIgnoredMessage`) is removed. A warn log fires once per compress when the user explicitly set `pruneNotificationType: "chat"` so the behavior change is discoverable. (2) `lib/messages/utils.ts:232-269` — `dropEmptyMessages` now treats text parts carrying `ignored: true` as contributing to emptiness, so any future ignored-only user message is dropped before reaching the provider (defense in depth). (3) `lib/config.ts:175` — default `pruneNotificationType` changed from `"chat"` to `"toast"` so default-configured users see no deprecation warning.
485
+
486
+ **Problem (cc-alg extraction, #168)**: Reusable compression algorithms (ROUGE-1 quality gate, hand-rolled tokenizer, trigger policy, compression-rules prompt) were locked inside ACP's AGPL codebase, preventing MIT-licensed reuse by other projects. Internal coupling between algorithm modules and ACP plumbing made independent testing and reuse difficult.
487
+
488
+ **Fix (cc-alg extraction, #168)**: 4 modules extracted to external MIT package `context-compress-algorithms@1.0.0` (npm: https://www.npmjs.com/package/context-compress-algorithms, GitHub: https://github.com/ranxianglei/context-compress-algorithms). ACP imports via `^1.0.0` and inline-bundles into `dist/index.js` via tsup `noExternal`, so hosts install no extra dependency. NOTICE file added for MIT attribution of bundled code. New trigger policy registry at `lib/messages/inject/policy/` enables host-side customization of nudge trigger behavior (default policy comes from cc-alg's `defaultTriggerPolicy`). Provenance audit confirmed zero derivation from DCP upstream (AGPL-3.0) — search for `rouge` / `qualityGate` / `computeShouldNudge` / `HOW_TO_COMPRESS_RULES` against DCP repo all returned 0 hits, so MIT extraction is legally safe. AGENTS.md updated with new Git Safety Rule forbidding `version` field bumps on non-release branches to prevent future version-number drift.
489
+
490
+ Files (compress notification): `lib/ui/notification.ts`, `lib/messages/utils.ts`, `lib/config.ts`, `tests/drop-empty-messages.test.ts`.
491
+ Files (cc-alg extraction): `lib/compress/quality-gate/tokenizer.ts` (deleted), `lib/compress/quality-gate/algorithms/rouge-recall-v1.ts` (deleted), `lib/prompts/compression-rules.ts` (deleted), `lib/compress/quality-gate/{index,algorithms/index}.ts` (re-export from cc-alg for backward compat), `lib/messages/inject/{inject,utils}.ts`, `lib/messages/inject/policy/{types,registry,index}.ts` (new), `lib/prompts/{system,context-limit-nudge,turn-nudge,iteration-nudge}.ts`, `package.json`, `tsup.config.ts`, `NOTICE` (new), `AGENTS.md`. Tests: `tests/quality-gate-tokenizer.test.ts` (deleted, moved to cc-alg), `tests/quality-gate-rouge-recall-v1.test.ts` (deleted, moved to cc-alg — 55 tests migrated), `tests/quality-gate-pipeline-integration.test.ts` (uses inline stub gate), `tests/trigger-policy-integration.test.ts` (new). 794 tests pass (cc-alg has its own 95 tests in its own repo).
492
+
493
+ ### v1.13.0 — Pluggable Quality Gate (Issue #20)
494
+
495
+ **Problem**: ACP had no mechanism to detect summaries that catastrophically lost content. The model could compress a 5K-token range into a 147-char summary missing every technical keyword, and the system would silently accept it. Issue #20 (calibrated against 6,913 real-world blocks from real sessions) identified two distinct failure modes: (1) length-floor failures — summaries <1% the size of the original (caught with 100% recall, 0% FPR via simple char count), and (2) content-coverage failures — summaries long enough to pass the length floor but capturing none of the original's keywords (e.g., a 996-char summary of a 5K-token original that recovers 0.6% of content words).
496
+
497
+ **Fix**: Added a pluggable `qualityGate` subsystem under `lib/compress/quality-gate/` with a `QualityGate` interface (`name`, `version`, `description`, `evaluate(ctx, config)`). The pipeline calls `evaluateBatchQuality()` in `finalizeSession()` after state is saved — failures only emit `logger.warn` (non-blocking: the compression result is already committed). The default algorithm `rouge-recall-v1` is a two-layer gate: L1 is a length/retention floor (200 chars AND 1% retention), L2 is an AND-combine of ROUGE-1 F1 < 0.05 AND top-20 keyword recall < 0.20 (AND keeps FPR at ~6.6% while still catching the long-but-empty failure mode). Tokenizer is hand-rolled word-level (English keywords ≥4 chars + Chinese unigrams/bigrams) — separate from ACP's BPE tokenizer, which is too coarse for ROUGE matching. Config defaults `enabled: false` for one release of burn-in. Interface leaves room for future algorithms including external API judges (sync signature today; future async gates need either type widening or internal wait+timeout wrap, registry/config unchanged).
498
+
499
+ Files: `lib/compress/quality-gate/{types,registry,tokenizer,evaluate,index}.ts`, `lib/compress/quality-gate/algorithms/{rouge-recall-v1,index}.ts`, `lib/compress/pipeline.ts`, `lib/config.ts`, `lib/config-validation.ts`, `dcp.schema.json`. Tests: `tests/quality-gate-{tokenizer,registry,rouge-recall-v1,pipeline-integration}.test.ts` (NEW, 74 tests). 842 tests pass.
500
+
501
+ ### v1.12.11 — README Refresh (PR #164)
502
+
503
+ **Problem**: README documentation had drifted from code reality. The tagline under-emphasized ACP's core capability. The "Deletion strategy" section in the English README described a feature that no longer exists and contradicted the Chinese version. Cache hit rate and context usage statistics were stale (87%, ~30%). The `compress.protectedTools` default was documented as 5 tools (`task, skill, todowrite, todoread, decompress`) when the actual code default is only `skill`.
504
+
505
+ **Fix**: (1) Added `<strong>200K tokens is enough.</strong>` tagline alongside the existing one-liner. (2) Refreshed "Proven at scale" table with real API-level data from 6 active sessions (Duration, Messages, API calls, Cumulative tokens, Cache hit %, P50/P90/P95 context), with outlier annotated. Aggregate cache hit ~91%. (3) Replaced "Deletion strategy" section (EN) with "GC safety net" matching the Chinese version. (4) Updated "How It Works" to list `acp_status` and `search_context` as supporting tools. (5) Updated cache stats: 87% → 91%, context ~30% → ~10–15% (p50 100K, p90 150K of 1M window). (6) Corrected `compress.protectedTools` default documentation to `skill` only (matches `COMPRESS_DEFAULT_PROTECTED_TOOLS` at `lib/config.ts:121`).
506
+
507
+ Files: `README.md`, `README.zh-CN.md`. No code changes. Tests: 768 pass (unchanged).
508
+
425
509
  ### v1.12.10 — Batch Compress + Decompress Range Mode + GC Memory-Loss Fix + Token Classification + Nudge Quality (PRs #73, #155, #156, #157, #158, #159, #161)
426
510
 
427
511
  **Problem**: Seven issues across compression UX, token accounting, GC safety, and nudge quality. (1) `decompress` required a per-block `acp_status` → decompress-per-block loop to restore multiple compressed blocks. (2) Since v1.12.9 (compress-as-anchor), compress tool `summary` content was misclassified as `toolTokens` instead of `summaryTokens`, inflating tool% and deflating summary% in the context breakdown. (3) The `compress` tool only accepted a single range per call — the model had to issue multiple calls to compress unrelated ranges, wasting turns. (4) The `[PROTECTED: ...]` label listed every tool in a protected message instead of only the triggering tools. (5) When all visible content was protected, the nudge still fired with an empty recommendation list. (6) When a nudge was suppressed, the next-turn check re-evaluated every turn. (7) **The GC system was silently destroying model-written summaries**: any block with `summary.length > 6000` chars was force-truncated to 3000 regardless of context pressure (0% pressure triggered truncation), and blocks with high `survivedCount` were auto-deactivated — causing irrecoverable memory loss across hundreds of sessions.
@@ -440,7 +524,7 @@ Files: `lib/messages/prune.ts`, `lib/messages/utils.ts`, `lib/compress/recap.ts`
440
524
 
441
525
  ### v1.12.8 — Phantom Block Rejection (PR #148)
442
526
 
443
- **Problem**: When the model called `compress` on a range that was already covered by an active compression block, `applyCompressionState` still created a new block with `directMessageIds: []`, `compressedTokens: 0`, and `effectiveMessageIds` inherited from the consumed block. The model saw "0 tokens removed" in the notification, retried the same range, and entered a death loop: each phantom block added ~1K of summary overhead while compressing nothing, causing context to *grow* with every compression call (issues #93, #135). User sessions showed 9 consecutive phantom compressions (b12–b20) on the same range before the user manually intervened.
527
+ **Problem**: When the model called `compress` on a range that was already covered by an active compression block, `applyCompressionState` still created a new block with `directMessageIds: []`, `compressedTokens: 0`, and `effectiveMessageIds` inherited from the consumed block. The model saw "0 tokens removed" in the notification, retried the same range, and entered a death loop: each phantom block added ~1K of summary overhead while compressing nothing, causing context to _grow_ with every compression call (issues #93, #135). User sessions showed 9 consecutive phantom compressions (b12–b20) on the same range before the user manually intervened.
444
528
 
445
529
  **Fix**: Added `checkPhantomBlock()` — a stateless pre-check in `lib/compress/pipeline.ts` that mirrors `applyCompressionState`'s `newlyCompressedMessageIds` computation. For each plan, it builds the effective message set (plan messages + consumed blocks' effective messages) and checks whether ANY message is "new" (i.e., has no active block covering it BEFORE mutation). If no message is new, the plan is a phantom and the entire compress call is rejected with a clear error before any state mutation occurs. Wired into both range-mode (`compress/range.ts`) and message-mode (`compress/message.ts`) after plan preparation, before snapshot. 12 tests cover: empty plans, all-new messages, consumed-block inheritance, GC'd messages (deactivated blocks count as new), and the exact `applyCompressionState` mirroring.
446
530
 
@@ -569,6 +653,7 @@ This release fixes two critical compression-injection bugs (#20 echo, #78 drift)
569
653
  #### Tool-Result Recap Injection — Fixes #20 & #78 (PR #95)
570
654
 
571
655
  **Problem**: Compression summaries were injected as text-based `role:assistant` or `role:user` messages. Both roles misled the model:
656
+
572
657
  - `role:assistant` (Bug 37 path) → model treated summaries as its own prior output and echoed them verbatim (#20, GLM-5.2).
573
658
  - `role:user` (Bug 36 merge path) → model treated summaries as user instructions and chased old topics (#78, gpt-5.5).
574
659
 
@@ -611,6 +696,7 @@ Fixes the over-compression bug reported in issue #18 and GitHub #85, where the `
611
696
  Additionally, the `compress.toolOutputNudgeThreshold` config key was **dead** — declared in the type but missing from the config merge, validation list, and JSON schema, so user overrides were silently dropped.
612
697
 
613
698
  **Fix** (4 coordinated changes):
699
+
614
700
  - `lib/messages/inject/inject.ts`: `toolOutputThreshold` now defaults to `nudgeGrowthTokens` (adaptive) instead of the hardcoded `5000`.
615
701
  - `lib/config.ts` `mergeCompress`: `toolOutputNudgeThreshold` override now flows through the config merge.
616
702
  - `lib/config-validation.ts`: `compress.toolOutputNudgeThreshold` registered as a valid config key.
package/README.zh-CN.md CHANGED
@@ -4,6 +4,8 @@
4
4
  <strong>Active Context Pruning</strong> — <a href="https://opencode.ai">OpenCode</a> 的主动上下文剪枝插件
5
5
  <br />
6
6
  由模型决定<em>何时</em>压缩、压缩<em>什么</em> — 而非硬性截断。
7
+ <br />
8
+ <strong>20 万 token 足矣。</strong>
7
9
  </p>
8
10
 
9
11
  ---
@@ -26,8 +28,8 @@ ACP 将上下文管理的所有权限全部交给模型自己,而不依靠外
26
28
 
27
29
  这带来两个影响:
28
30
 
29
- - **省 token(约三分之二)。** 一个 100 token 上下文窗口的模型,实际只在 **20 万–30 万 token** 区间运行。
30
- - **超长上下文不丢关键内容** —— 支持 **5 亿级别上下文、单会话 10 万条消息**。
31
+ - **20 token 足矣。** 50 个真实工程会话、3 万余次 API 调用中,**97% 的请求低于 20 万 token** —— p90 约 15 万,p95 约 18 万。每次 API 调用都会对完整上下文重新计费,因此上下文越低越省钱 —— 即使缓存命中率达 90%+,未缓存部分仍按全价计费。
32
+ - **超长上下文不丢关键内容** —— 实测单会话 **3,300+ 条消息、3 亿+ 累计 token**;架构上支持单会话 **10 万条消息**(5 位消息 ID 空间)。
31
33
 
32
34
  ---
33
35
 
@@ -35,19 +37,18 @@ ACP 将上下文管理的所有权限全部交给模型自己,而不依靠外
35
37
 
36
38
  真实工程中的上下文情况。
37
39
 
38
- **支持 5 亿级别 token,p95 上下文比例在 30% 左右,平均缓存命中率 85% 以上。**(注意这是平均缓存命中率,不是单会话命中率——后面[对 Prompt 缓存的影响](#对-prompt-缓存的影响)会解释,这实际上比传统压缩算法大幅度节省了 token。)
40
+ **在 6 个活跃工程会话(11,000+ 次 API 调用)中,上下文 p90 稳定在 15 万–19 万(15–19%),p95 16 万–21 万(16–21%)—— 聚合缓存命中率达 91%。**(注意这是平均缓存命中率,不是单会话命中率——后面[对 Prompt 缓存的影响](#对-prompt-缓存的影响)会解释,这实际上比传统压缩算法大幅度节省了 token。)
41
+
42
+ | 会话 | 时长 | 消息数 | API 调用 | 累计 token | 缓存命中率 | 上下文 p50 | 上下文 p90 | 上下文 p95 |
43
+ | --------- | ----------- | ------ | -------- | ---------- | ---------- | ----------- | ----------- | ----------- |
44
+ | 0b89319b | 230h (9.5d) | 3,344 | 2,796 | 3.39 亿 | 93% | 10.8万(11%) | 16.7万(17%) | 21.0万(21%) |
45
+ | 0a3be0cd | 130h (5.4d) | 3,183 | 2,499 | 2.76 亿 | 91% | 10.4万(10%) | 14.5万(15%) | 15.3万(15%) |
46
+ | 0b2cd5a7 | 131h (5.4d) | 2,560 | 2,181 | 3.14 亿 | 91% | 14.2万(14%) | 19.1万(19%) | 19.7万(20%) |
47
+ | 08f2d501 | 37h (1.5d) | 1,985 | 1,888 | 1.96 亿 | 95% | 10.0万(10%) | 15.6万(16%) | 16.8万(17%) |
48
+ | 1410c791† | 865h (36d) | 1,279 | 1,100 | 2.18 亿 | 87% | 13.2万(13%) | 40.7万(41%) | 42.7万(43%) |
49
+ | 096cf8c4 | 72h (3d) | 1,041 | 918 | 0.91 亿 | 89% | 9.2万(9%) | 14.8万(15%) | 16.1万(16%) |
39
50
 
40
- | | 会话一 | 会话二 |
41
- | ----------------------- | -------------- | -------------- |
42
- | **消息总条数** | 3,024 | 2,028 |
43
- | **累计处理 token** | 5.82 亿 | 4.63 亿 |
44
- | **prompt-cache 命中率** | 86.2% | 89.0% |
45
- | **上下文 p50(中位)** | 1.2 K(<1%) | 1.8 K(<1%) |
46
- | **上下文 p75** | 2.8 K | 3.5 K |
47
- | **上下文 p90** | 10.8 万(11%) | 5.8 万(6%) |
48
- | **上下文 p95** | 25.1 万(25%) | 33.5 万(34%) |
49
- | **上下文 p99** | 42.5 万(43%) | 44.2 万(44%) |
50
- | **峰值** | 48.8 万(49%) | 76.9 万(77%) |
51
+ Bug 测试会话,p95 异常偏高。排除该会话后,其余会话 p95 均 ≤ 21 万。
51
52
 
52
53
  (上下文百分比均以 1M 窗口计。)
53
54
 
@@ -73,7 +74,7 @@ opencode plugin opencode-acp@latest --global
73
74
 
74
75
  ## 工作原理
75
76
 
76
- ACP 把上下文压缩工具直接交给模型。模型对上下文压缩**负全责**。模型可用的工具主要是:**compress** 和 **decompress**。当上下文达到 100% 时,系统自动触发 GC 截断作为兜底。
77
+ ACP 把上下文压缩工具直接交给模型。模型对上下文压缩**负全责**。模型的主要工具是 **compress** 和 **decompress**,辅以 **acp_status**(上下文监控)和 **search_context**(搜索已压缩内容)。当上下文达到 100% 时,系统自动触发 GC 截断作为兜底。
77
78
 
78
79
  ### 生命周期
79
80
 
@@ -108,15 +109,37 @@ stateDiagram-v2
108
109
 
109
110
  当上下文达到 100% 时,系统自动截断老年代 block 摘要,防止上下文溢出。这是最后的兜底机制,不影响模型的正常压缩/解压操作。
110
111
 
112
+ ### 质量门控(非阻塞,默认关闭)
113
+
114
+ 每次 `compress` 调用之后,ACP 可以运行一个可拔插的质量门控,检测摘要是否灾难性丢失了内容(例如:5K token 的范围被压缩成 147 字符、且不含任何技术关键词的摘要)。失败只发出 `logger.warn`——绝不拒绝压缩(结果已经提交到状态、对模型可见)。
115
+
116
+ 默认算法 `rouge-recall-v1` 是基于 6,913 个真实块校准的两层门控:
117
+
118
+ - **L1(长度下限)**:捕获灾难性留存失败——摘要短于 200 字符 OR 留存率低于原始的 1%。100% 召回,0% 误报。
119
+ - **L2(内容覆盖)**:只在通过 L1 的块上运行。当 **同时** ROUGE-1 F1 < 0.05 **且** top-20 关键词召回 < 0.20 时触发(AND 合并使误报率维持在 ~6.6%)。
120
+
121
+ 接口可拔插:未来算法(例如通过外部 API 的 LLM-as-judge)可通过 `registerQualityGate()` 注册,无需改动 pipeline 接线。分词器使用手写的词级分词(英文关键词 + 中文 unigram/bigram),不使用 ACP 的 BPE 分词器——后者对 ROUGE 风格的匹配粒度过粗。
122
+
123
+ 默认关闭,发布一个版本的烧入期后再开启。启用方式:
124
+
125
+ ```jsonc
126
+ {
127
+ "qualityGate": {
128
+ "enabled": true,
129
+ "algorithm": "rouge-recall-v1",
130
+ },
131
+ }
132
+ ```
133
+
111
134
  ---
112
135
 
113
136
  ## 对 Prompt 缓存的影响
114
137
 
115
- 历史上 ACP 修复了大量由 DCP 导致的低缓存命中率问题。目前整体缓存命中率约为 **87%**。
138
+ 历史上 ACP 修复了大量由 DCP 导致的低缓存命中率问题。目前整体缓存命中率约为 **91%**。
116
139
 
117
140
  相比传统压缩——只在 80–90% 时才压缩,一旦压缩就强制 100% 的上下文重新命中——ACP 的命中率实际上更高。
118
141
 
119
- 此外:ACP 大部分时间将总上下文维持在 **~30%** 左右,而传统方案是 50–80%。因此总 token 节省远高于传统压缩。
142
+ 此外:ACP 大部分时间将总上下文维持在 **~10–15%**(p50 10 万、p90 15 万,以 1M 窗口计),而传统方案是 50–80%。因此总 token 节省远高于传统压缩。
120
143
 
121
144
  **结论:** ACP 在提高整体缓存命中率的同时,确保关键上下文信息不丢失。
122
145
 
@@ -182,8 +205,8 @@ ACP 使用自己的配置文件,按以下顺序搜索:
182
205
  "debug": false,
183
206
  // Notification display: "off", "minimal", or "detailed"
184
207
  "pruneNotification": "detailed",
185
- // Notification type: "chat" (in-conversation) or "toast" (system toast)
186
- "pruneNotificationType": "chat",
208
+ // Notification type: "chat" (deprecated, falls back to toast) or "toast" (system toast)
209
+ "pruneNotificationType": "toast",
187
210
  // Slash commands configuration
188
211
  "commands": {
189
212
  "enabled": true,
@@ -292,6 +315,27 @@ ACP 使用自己的配置文件,按以下顺序搜索:
292
315
  // 上下文使用率超过此值时执行主 GC(兜底,硬编码为 100%)
293
316
  "majorGcThresholdPercent": "100%",
294
317
  },
318
+ // 压缩后质量门控(非阻塞;默认关闭)
319
+ "qualityGate": {
320
+ // 主开关。false 时不执行任何评估
321
+ "enabled": false,
322
+ // 算法名。可拔插——未来算法(包括外部 API 评审)可以注册而无需改 pipeline
323
+ "algorithm": "rouge-recall-v1",
324
+ // 各算法的独立配置
325
+ "algorithms": {
326
+ "rouge-recall-v1": {
327
+ // 摘要长度硬下限(字符)。低于此值 → L1 失败
328
+ "layer1MinChars": 200,
329
+ // 最小留存率 = summaryLen / (compressedTokens*4) * 100
330
+ // 捕获灾难性留存失败(<1%),0% 误报
331
+ "layer1MinRetentionPct": 1.0,
332
+ // 当 ROUGE-1 F1 低于此值时(与 top20Recall 经 AND 合并)L2 失败
333
+ "layer2MaxRougeF1": 0.05,
334
+ // 当 top-20 关键词召回低于此值时(与 rougeF1 经 AND 合并)L2 失败
335
+ "layer2MaxTop20Recall": 0.20,
336
+ },
337
+ },
338
+ },
295
339
  }
296
340
  ```
297
341
 
@@ -323,7 +367,7 @@ ACP 暴露六个可编辑的 prompt:
323
367
 
324
368
  `commands` 和 `strategies` 中的 `protectedTools` 数组会添加到此默认列表。
325
369
 
326
- 对于 `compress` 工具,`compress.protectedTools` 确保特定工具的输出被**硬排除**在压缩范围之外(v1.10.0+)。当模型压缩包含受保护工具消息的范围时,该消息完整保留在可见上下文中 — 只有周围的非受保护消息被压缩。默认包含 `task`、`skill`、`todowrite`、`todoread` `decompress`。
370
+ 对于 `compress` 工具,`compress.protectedTools` 确保特定工具的输出被**硬排除**在压缩范围之外(v1.10.0+)。当模型压缩包含受保护工具消息的范围时,该消息完整保留在可见上下文中 — 只有周围的非受保护消息被压缩。默认仅包含 `skill` —— 实践中这一个就够了,因为 skill 输出是唯一绝不能被压缩丢失的工具类型。
327
371
 
328
372
  ---
329
373
 
@@ -395,6 +439,41 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
395
439
 
396
440
  ## 更新日志
397
441
 
442
+ ### v1.13.2 — 保留最近用户消息 + 配置默认值调优(PR #169)
443
+
444
+ **问题**:v1.13.1 的通知冻结修复之后还剩两个问题。(1)当模型压缩的范围覆盖了所有可见的 user 消息时,下一次 API 调用中 user 角色消息数量为零——zhipuai-lb 以同样的 HTTP 400 code 1214(`isRetryable: false`)拒绝,会话冻结。这是 v1.13.1 修复的空通知路径之外,通往同一冻结 bug 的第二条路径。(2)默认 `pruneNotification: "detailed"` 每次压缩都弹 toast(典型会话 10–30 次),对例行后台操作来说过于打扰。另外 `compress.maxSummaryLengthHard: 10000` 在真实会话中拒绝了约 25% 信息密度高的有用摘要。
445
+
446
+ **修复**:(1)`lib/messages/prune.ts`——`filterCompressedRanges` 重写为两段过滤:第一段计算存活消息,第二段构建结果;如果没有 user 角色消息存活,恢复最近一条被压缩的 user 消息以保证 API 请求格式合法。恢复仅发生在 transform 阶段——`byMessageId` 仍记录该消息为已压缩。(2)`lib/config.ts`——默认 `pruneNotification` 改为 `"off"`;压缩事件仍通过 `lib/ui/notification.ts` 新增的 always-log 路径记录到 `~/.config/opencode/logs/acp/`(无损失可观测性,无 UI 噪音)。(3)`lib/config.ts`——默认 `compress.maxSummaryLengthHard` 从 `10000` 提升到 `20000`(与真实会话中观察到的优质摘要长度对齐)。(4)`dcp.schema.json`——同步 4 个过时默认值。文件:`lib/messages/prune.ts`、`lib/config.ts`、`lib/ui/notification.ts`、`dcp.schema.json`、`README.md`。测试:803 pass(5 个新的 preserve-last-user 回归测试)。
447
+
448
+ ### v1.13.1 — cc-alg 抽取 + 压缩通知冻结修复(PR #167, #168)
449
+
450
+ **问题(压缩通知冻结,#167)**:每次 `compress` 工具调用成功后,ACP 会注入一条 user 角色通知消息,其中只包含一个带 `ignored: true` 标记的 text part。opencode 在发送给 LLM 前会剥离 ignored parts,于是这条消息变成空 user 消息。Provider(zhipuai-lb / glm-5.2)会以 HTTP 400 code 1214(`"messages 参数非法"`)拒绝,且 `isRetryable: false`——opencode 不会重试,会话冻结,直到外部恢复。所有活跃会话累计发生 113 次(单个 3,156 条消息的会话出现 8 次)。
451
+
452
+ **修复(压缩通知冻结,#167)**:(1)`lib/ui/notification.ts:280-298`——`sendCompressNotification` 改为始终调用 `client.tui.showToast`;移除了原本调用 `sendIgnoredMessage` 的 `chat` 分支。当用户显式配置 `pruneNotificationType: "chat"` 时,输出一次 warn 日志以便发现行为变化。(2)`lib/messages/utils.ts:232-269`——`dropEmptyMessages` 现在将带 `ignored: true` 的 text part 也视为"可丢弃",任何未来出现的 ignored-only user 消息会在到达 provider 之前被丢弃(纵深防御)。(3)`lib/config.ts:175`——默认 `pruneNotificationType` 从 `"chat"` 改为 `"toast"`,使用默认配置的用户不会看到弃用警告。
453
+
454
+ **问题(cc-alg 抽取,#168)**:可复用的压缩算法(ROUGE-1 质量门控、手写 tokenizer、trigger policy、compression-rules prompt)锁在 ACP 的 AGPL 代码库里,其他项目无法以 MIT 协议复用。算法模块与 ACP 管道的内部耦合也使独立测试和复用困难。
455
+
456
+ **修复(cc-alg 抽取,#168)**:4 个模块抽取到独立 MIT 包 `context-compress-algorithms@1.0.0`(npm:https://www.npmjs.com/package/context-compress-algorithms,GitHub:https://github.com/ranxianglei/context-compress-algorithms)。ACP 通过 `^1.0.0` 引用,并用 tsup `noExternal` 把 cc-alg inline-bundle 进 `dist/index.js`,host 不需要安装额外依赖。新增 `NOTICE` 文件按开源合规要求随包发布 MIT 归属。新增 `lib/messages/inject/policy/` 注册表支持 host 端自定义 nudge trigger 行为(默认 policy 来自 cc-alg 的 `defaultTriggerPolicy`)。Provenance 审计确认对 DCP 上游(AGPL-3.0)零 derivation——在 DCP 仓库搜索 `rouge` / `qualityGate` / `computeShouldNudge` / `HOW_TO_COMPRESS_RULES` 均为 0 hit,MIT 抽取法律安全。AGENTS.md 新增 Git 安全规则:非 release 分支禁止修改 `version` 字段,避免今后版本号再次被改乱。
457
+
458
+ 文件(压缩通知):`lib/ui/notification.ts`、`lib/messages/utils.ts`、`lib/config.ts`、`tests/drop-empty-messages.test.ts`。
459
+ 文件(cc-alg 抽取):`lib/compress/quality-gate/tokenizer.ts`(删除)、`lib/compress/quality-gate/algorithms/rouge-recall-v1.ts`(删除)、`lib/prompts/compression-rules.ts`(删除)、`lib/compress/quality-gate/{index,algorithms/index}.ts`(从 cc-alg re-export,向后兼容)、`lib/messages/inject/{inject,utils}.ts`、`lib/messages/inject/policy/{types,registry,index}.ts`(新增)、`lib/prompts/{system,context-limit-nudge,turn-nudge,iteration-nudge}.ts`、`package.json`、`tsup.config.ts`、`NOTICE`(新增)、`AGENTS.md`。测试:`tests/quality-gate-tokenizer.test.ts`(删除,迁到 cc-alg)、`tests/quality-gate-rouge-recall-v1.test.ts`(删除,迁到 cc-alg——55 个测试迁移)、`tests/quality-gate-pipeline-integration.test.ts`(改用 inline stub gate)、`tests/trigger-policy-integration.test.ts`(新增)。794 个测试通过(cc-alg 自身在独立仓库有 95 个测试)。
460
+
461
+ ### v1.13.0 — 可拔插质量门控(Issue #20)
462
+
463
+ **问题**:ACP 没有机制检测摘要是否灾难性丢失了内容。模型可能把 5K token 的范围压缩成 147 字符、且不含任何技术关键词的摘要,系统也会默默接受。Issue #20(基于真实会话的 6,913 个块校准)识别出两种不同的失败模式:(1) 长度下限失败——摘要 < 原始的 1%(通过简单的字符计数即可 100% 召回,0% 误报);(2) 内容覆盖失败——摘要长度足够通过长度门,但没有捕获原始内容中的任何关键词(例如:5K token 原始内容的 996 字符摘要只恢复了 0.6% 的内容词)。
464
+
465
+ **修复**:在 `lib/compress/quality-gate/` 下添加可拔插的 `qualityGate` 子系统,定义 `QualityGate` 接口(`name`、`version`、`description`、`evaluate(ctx, config)`)。pipeline 在 `finalizeSession()` 中状态保存之后调用 `evaluateBatchQuality()`——失败仅发出 `logger.warn`(非阻塞:压缩结果已经提交)。默认算法 `rouge-recall-v1` 是两层门控:L1 是长度/留存下限(200 字符 AND 1% 留存),L2 是 ROUGE-1 F1 < 0.05 与 top-20 关键词召回 < 0.20 的 AND 合并(AND 把误报率维持在 ~6.6%,同时仍能捕获"长但空"的失败模式)。分词器为手写的词级分词(英文关键词 ≥4 字符 + 中文 unigram/bigram),与 ACP 的 BPE 分词器分离——后者对 ROUGE 匹配粒度过粗。配置默认 `enabled: false`,发布一个版本的烧入期。接口为未来算法留出空间,包括外部 API 评审(当前为同步签名;未来异步门控需要扩展类型或在内部 wait+timeout 包装,registry/config 不变)。
466
+
467
+ 文件:`lib/compress/quality-gate/{types,registry,tokenizer,evaluate,index}.ts`、`lib/compress/quality-gate/algorithms/{rouge-recall-v1,index}.ts`、`lib/compress/pipeline.ts`、`lib/config.ts`、`lib/config-validation.ts`、`dcp.schema.json`。测试:`tests/quality-gate-{tokenizer,registry,rouge-recall-v1,pipeline-integration}.test.ts`(新增,74 个测试)。842 个测试通过。
468
+
469
+ ### v1.12.11 — README 文档刷新(PR #164)
470
+
471
+ **问题**:README 文档与代码实际脱节。tagline 没有突出 ACP 的核心能力。英文版的 "Deletion strategy" 章节描述了一个已不存在的功能,且与中文版不一致。缓存命中率和上下文使用率数据过期(87%、~30%)。`compress.protectedTools` 默认值文档列出了 5 个工具(`task, skill, todowrite, todoread, decompress`),但代码实际默认值只有 `skill`。
472
+
473
+ **修复**:(1)新增 `<strong>20 万 token 足矣。</strong>` tagline。(2)"Proven at scale" 表格更新为 6 个活跃会话的真实 API 级数据(运行时长、消息数、API 调用数、累计 token、缓存命中率、P50/P90/P95),离群值标注。(3)英文版 "Deletion strategy" 替换为 "GC safety net",与中文版 "GC 兜底" 一致。(4)"工作原理" 列出 `acp_status` 和 `search_context` 为辅助工具。(5)缓存统计更新:87% → 91%,上下文 ~30% → ~10–15%(1M 窗口的 p50 10 万、p90 15 万)。(6)`compress.protectedTools` 默认值文档更正为仅 `skill`(匹配 `lib/config.ts:121` 的 `COMPRESS_DEFAULT_PROTECTED_TOOLS`)。
474
+
475
+ 文件:`README.md`、`README.zh-CN.md`。无代码改动。测试:768 通过(不变)。
476
+
398
477
  ### v1.12.10 — 批量压缩 + Decompress 范围模式 + GC 记忆丢失修复 + Token 分类 + Nudge 质量(PR #73, #155, #156, #157, #158, #159, #161)
399
478
 
400
479
  **问题**:七个问题,涉及压缩 UX、token 统计、GC 安全和 nudge 质量。(1)`decompress` 需要先 `acp_status` 再逐块 decompress 的循环才能恢复多个压缩块。(2)自 v1.12.9 起,compress 工具的 `summary` 内容被错误分类为 `toolTokens` 而非 `summaryTokens`,导致上下文分布中 tool% 虚高、summary% 虚低。(3)`compress` 工具每次调用只能压缩一个范围 —— 模型需要多次调用才能压缩不相关的范围,浪费轮次。(4)`[PROTECTED: ...]` 标签列出受保护消息中的所有工具而非仅触发保护的工具。(5)当所有可见内容都是受保护内容时,nudge 仍然以空推荐列表注入。(6)当 nudge 被抑制时,下一轮检查每轮都重新评估。(7)**GC 系统在静默销毁模型编写的 summary**:任何 `summary.length > 6000` 字符的块在零上下文压力下被强制截断到 3000,且 `survivedCount` 过高的块被自动 deactivate —— 导致数百个会话的不可恢复记忆丢失。
@@ -542,6 +621,7 @@ Issue #23(上下文内存泄露)的综合修复。7 个 commit,22 个文
542
621
  #### 工具结果注入 — 修复 #20 & #78(PR #95)
543
622
 
544
623
  **问题**:压缩摘要以文本形式的 `role:assistant` 或 `role:user` 消息注入。两种角色都会误导模型:
624
+
545
625
  - `role:assistant`(Bug 37 路径)→ 模型将摘要当作自己的前文,逐字复读(#20,GLM-5.2)。
546
626
  - `role:user`(Bug 36 合并路径)→ 模型将摘要当作用户指令,去执行旧话题(#78,gpt-5.5)。
547
627