opencode-acp 1.13.9-dev.1 → 1.14.0

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 (36) hide show
  1. package/README.md +72 -11
  2. package/README.zh-CN.md +55 -8
  3. package/dist/index.js +592 -66
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/commands/compression-targets.d.ts.map +1 -1
  6. package/dist/lib/commands/recompress.d.ts.map +1 -1
  7. package/dist/lib/commands/stats.d.ts.map +1 -1
  8. package/dist/lib/compress/decompress-logic.d.ts +3 -1
  9. package/dist/lib/compress/decompress-logic.d.ts.map +1 -1
  10. package/dist/lib/compress/decompress.d.ts.map +1 -1
  11. package/dist/lib/compress/hide-consumed.d.ts +17 -0
  12. package/dist/lib/compress/hide-consumed.d.ts.map +1 -0
  13. package/dist/lib/compress/index.d.ts +1 -0
  14. package/dist/lib/compress/index.d.ts.map +1 -1
  15. package/dist/lib/compress/pipeline.d.ts +11 -4
  16. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  17. package/dist/lib/compress/state.d.ts.map +1 -1
  18. package/dist/lib/compress/status.d.ts.map +1 -1
  19. package/dist/lib/config-validation.d.ts.map +1 -1
  20. package/dist/lib/config.d.ts +6 -0
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/hooks.d.ts.map +1 -1
  23. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  24. package/dist/lib/messages/inject/utils.d.ts +18 -0
  25. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  26. package/dist/lib/messages/sync.d.ts.map +1 -1
  27. package/dist/lib/prompts/system.d.ts +1 -1
  28. package/dist/lib/prompts/system.d.ts.map +1 -1
  29. package/dist/lib/state/persistence.d.ts +4 -0
  30. package/dist/lib/state/persistence.d.ts.map +1 -1
  31. package/dist/lib/state/state.d.ts.map +1 -1
  32. package/dist/lib/state/types.d.ts +12 -0
  33. package/dist/lib/state/types.d.ts.map +1 -1
  34. package/dist/lib/state/utils.d.ts +12 -1
  35. package/dist/lib/state/utils.d.ts.map +1 -1
  36. package/package.json +2 -2
package/README.md CHANGED
@@ -90,23 +90,76 @@ Or add to your opencode config:
90
90
  ACP hands the context-compression tool directly to the model. The model is
91
91
  **100% responsible** for context compression. The model's primary tools are
92
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.
93
+ and **search_context** (search compressed content). Compression uses a
94
+ **three-tier LSM-tree architecture** (T1 capture T2 distill T3 condense)
95
+ that keeps context bounded for years. A hardcoded 100% GC fallback acts as a
96
+ safety net when the context window is completely full.
95
97
 
96
- ### Lifecycle
98
+ ### Lifecycle — Three-Tier Compression
97
99
 
98
- Two operations: **compress** and **decompress**. Content loops between raw and
99
- compressed. When context hits 100%, old-gen block summaries are truncated as
100
- a last resort:
100
+ ACP uses a **three-tier LSM-tree compression architecture**, inspired by
101
+ database storage engines. Each tier compresses the previous tier's output,
102
+ creating progressively denser summaries with natural frequency decrease:
101
103
 
102
104
  ```mermaid
103
105
  stateDiagram-v2
104
- Raw --> Compressed : compress
105
- Compressed --> Raw : decompress
106
- Compressed --> Truncated : GC at 100%
106
+ Raw --> Tier1 : compress (every ~7 turns)
107
+ Tier1 --> Tier2 : distill (every ~250 turns)
108
+ Tier2 --> Tier3 : condense (every ~2500 turns)
109
+ Tier1 --> Raw : decompress
110
+ Tier2 --> Raw : decompress (recursive)
111
+ Tier3 --> Raw : decompress (recursive)
112
+ Tier1 --> GC_Truncated : GC at 100% context
107
113
  ```
108
114
 
109
- ### Compression strategy
115
+ | Tier | Name | Input | Output | Compression ratio | When it fires |
116
+ |------|------|-------|--------|-------------------|---------------|
117
+ | **T1** | Capture | Raw conversation | Detailed summary | ~45× | Context exceeds `maxContextLimit` |
118
+ | **T2** | Distill | T1 summaries (≥ `nudgeGrowthTokens`) | Condensed decisions/outcomes | ~10× | T1 summaries accumulate past threshold |
119
+ | **T3** | Condense | T2 summaries (≥ `nudgeGrowthTokens`) | Bare facts (1-3 per block) | ~5× | T2 summaries accumulate past threshold |
120
+
121
+ **How triggers work:**
122
+
123
+ - **T1** fires when raw context exceeds the configured limit. The model sees
124
+ compressible ranges and writes a detailed summary preserving file paths,
125
+ signatures, decisions, and rationale.
126
+ - **T2** fires when T1 summary tokens reach `nudgeGrowthTokens` (default 5% of
127
+ context window). The model distills old T1 blocks — keeping decisions and
128
+ outcomes, dropping verbose process details.
129
+ - **T3** fires when T2 summary tokens reach the same threshold. The model
130
+ condenses to bare facts (shipped releases, key bugs, architecture decisions).
131
+
132
+ Each tier has an **independent cadence counter** — T2 firing doesn't block T3.
133
+ T1 has priority via a `!shouldInject` guard: if T1 fires, T2/T3 wait until next
134
+ turn. This ensures raw context compression happens first (it has the biggest
135
+ impact).
136
+
137
+ **Session capacity** — total tokens a single session can process from empty → T1 →
138
+ T2 → T3 → context limit (real-calibrated: 500 API calls/day, ~9.6K new tokens/call,
139
+ T1=45x/T2=10x/T3=3x):
140
+
141
+ | Context limit | 1 month | 3 months | At limit | Limit reached |
142
+ |---------------|---------|----------|----------|---------------|
143
+ | 1M | 1.9B tok | 10.5B tok | **68.9B tok** | day 259 (~8.6 mo) |
144
+ | 400K | 1.9B tok | 10.3B tok | **10.3B tok** | day 89 (~3 mo) |
145
+ | 400K (200 calls/day) | 559M tok | 2.5B tok | **9.5B tok** | day 212 (~7 mo) |
146
+
147
+ **Token savings** — without ACP, context grows unbounded and the session crashes
148
+ after ~100 API calls (~0.2 days). With ACP, context is bounded by compression:
149
+
150
+ | Metric | Without ACP | With ACP (1M model) |
151
+ |--------|-------------|---------------------|
152
+ | Session lifetime | ~0.2 days | 259 days (**1295x** longer) |
153
+ | Total tokens processed | ~52M | 68.9B (**1325x** more work) |
154
+
155
+ The core value: ACP doesn't just reduce per-call token cost — it enables a single
156
+ session to process **1000x more total work** by keeping context bounded across
157
+ the full session lifetime.
158
+
159
+ The model uses the **same `compress` tool** for all tiers. T2/T3 compressions
160
+ use block IDs as boundaries (`compress({ content: [{ startId: "b5", endId: "b20", summary: "..." }] })`). Tier is auto-detected from consumed blocks.
161
+
162
+ ### Compression strategy (Tier 1)
110
163
 
111
164
  The system injects a prompt telling the model the current context ratio, the
112
165
  compression ratio, whether context is idle, and compression suggestions. When the
@@ -132,7 +185,7 @@ later work.
132
185
 
133
186
  ### GC safety net
134
187
 
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.
188
+ When context reaches 100%, the system automatically truncates old-gen block summaries to prevent overflow. This is a last-resort safety net with three-tier compression, the GC rarely activates because T2/T3 distillation keeps summary overhead bounded.
136
189
 
137
190
  ### Quality gate (non-blocking, off by default)
138
191
 
@@ -475,6 +528,14 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
475
528
 
476
529
  ## Changelog
477
530
 
531
+ ### v1.14.0 — Three-Tier Compression + Preserve-Recent + Summary Visibility Fix (PRs #200, #201, #202)
532
+
533
+ **Problem**: Three critical issues for long-session stability. (1) **Summary accumulation** (PR #200): Summary blocks accumulated indefinitely (v1.13.5+ force-protection). At ~7.3K tokens/day growth rate, sessions hit the 100K summary ceiling in ~3 days. 92.5% of ancient blocks were shipped/historical work with zero actionable value. (2) **Active task loss** (PR #201): `lastSegmentSoftBlock` only protected the very last 1 message from compression. When the model compressed a range that included the current task context, the active work was lost — the recommendation list itself could point at messages that should have been protected. (3) **summaryBuffer over-counting** (PR #202): `getActiveSummaryTokenUsage()` counted ALL active blocks (e.g., 448 = 151K tokens), but only ~26 blocks had their compress calls in the visible context window. The inflated count caused false T2/T3 triggers and misleading `acp stats` output ("摘要 146%").
534
+
535
+ **Fix**: (1) PR #200 — Implemented a **3-tier LSM-tree compression architecture** (T1 capture → T2 distill → T3 condense). Each tier compresses the previous tier's output with decreasing detail. Independent triggers: each tier fires when its input summaries reach `nudgeGrowthTokens`. T1 has priority via `!shouldInject` guard. Tier auto-detection from consumed blocks. Tier-aware decompress (default = one level up, `full:true` = recursive to raw). New `block.tier` field, `getTierTokenUsage()`, `hideConsumedCompressCalls()`, `effectiveCompressedTokens`, `deactivatedByUserDeep` flag. Also fixed `syncCompressionBlocks` to stop deactivating blocks when their anchor message scrolled out of context (1137 blocks across 21 sessions incorrectly deactivated). 919 tests pass. cc-alg v1.2.1 (pinned exact). Session capacity: 1M model processes 68.9B tokens over 259 days; 400K model 10.3B over 89 days. 6 rounds of dual-agent review (all findings fixed). (2) PR #201 — Added `preserveRecentMessages` (default 20), `preserveRecentTokens` (default 20000), `preserveLastUserMessage` (default true) to `compress` config. `computeProtectedRawIds` / `computeProtectedRefs` compute the protected zone; `excludeProtectedRanges` filters recommendation list; `checkProtectedRange` rejects compression attempts on protected messages. Nudge auto-suppressed when all ranges fall in protected zone. 880 tests pass. (3) PR #202 — `getActiveSummaryTokenUsage(state, visibleMessageIds?)` now accepts an optional filter. `isContextOverLimits` and `handleStatsCommand` pass `new Set(messages.map(m => m.info.id))` so only blocks whose `compressMessageId` is in the visible window are counted. Same fix applied to `collectVisibleMessages` in `lib/compress/status.ts`. 880 tests pass.
536
+
537
+ Files: `lib/state/{types,utils,state}.ts`, `lib/compress/{state,pipeline,decompress-logic,decompress,hide-consumed,status}.ts`, `lib/messages/inject/{inject,utils}.ts`, `lib/messages/sync.ts`, `lib/messages/prune.ts`, `lib/commands/{recompress,stats}.ts`, `lib/config.ts`, `lib/config-validation.ts`, `lib/prompts/system.ts`, `dcp.schema.json`. Tests: `tests/e2e-tier-{compression,simulation}.test.ts`, `tests/preserve-recent.test.ts`, `tests/summary-buffer-visibility.test.ts`, `tests/acp-status.test.ts`, `tests/decompress-logic.test.ts`, `tests/soft-block.test.ts`.
538
+
478
539
  ### v1.13.9-dev.1 — Remove Subagent History Rewriting (PR #180)
479
540
 
480
541
  **Problem**: `injectExtendedSubAgentResults` rewrote historical `<task_result>` tool outputs in the parent agent's message history on every transform run when `experimental.allowSubAgents: true`. The `subAgentResultCache` was cleared on every parent↔child session switch and was never persisted, so each transform run re-fetched the subagent session and produced a new historical message body — invalidating the provider prefix cache (observed: ~56% hit rate vs healthy 96–98%, prefix frozen at ~22K tokens).
package/README.zh-CN.md CHANGED
@@ -74,20 +74,59 @@ opencode plugin opencode-acp@latest --global
74
74
 
75
75
  ## 工作原理
76
76
 
77
- ACP 把上下文压缩工具直接交给模型。模型对上下文压缩**负全责**。模型的主要工具是 **compress** 和 **decompress**,辅以 **acp_status**(上下文监控)和 **search_context**(搜索已压缩内容)。当上下文达到 100% 时,系统自动触发 GC 截断作为兜底。
77
+ ACP 把上下文压缩工具直接交给模型。模型对上下文压缩**负全责**。模型的主要工具是 **compress** 和 **decompress**,辅以 **acp_status**(上下文监控)和 **search_context**(搜索已压缩内容)。压缩采用**三级 LSM-tree 架构**(T1 捕获 → T2 蒸馏 → T3 浓缩),使上下文在数年内保持有界。当上下文达到 100% 时,系统自动触发 GC 截断作为兜底。
78
78
 
79
- ### 生命周期
79
+ ### 生命周期 — 三级压缩
80
80
 
81
- 两个操作:**压缩**、**解压缩**。内容在原始与压缩之间循环。当上下文达到 100% 时,GC 自动截断老年代 block 作为兜底:
81
+ ACP 采用**三级 LSM-tree 压缩架构**,灵感来自数据库存储引擎。每一级压缩上一级的输出,产生逐渐精炼的摘要,频率自然递减:
82
82
 
83
83
  ```mermaid
84
84
  stateDiagram-v2
85
- Raw --> Compressed : compress
86
- Compressed --> Raw : decompress
87
- Compressed --> GC_Truncated : GC (100%)
85
+ Raw --> Tier1 : compress(约每 7 轮)
86
+ Tier1 --> Tier2 : distill(约每 250 轮)
87
+ Tier2 --> Tier3 : condense(约每 2500 轮)
88
+ Tier1 --> Raw : decompress
89
+ Tier2 --> Raw : decompress(递归)
90
+ Tier3 --> Raw : decompress(递归)
91
+ Tier1 --> GC_Truncated : GC(100% 上下文)
88
92
  ```
89
93
 
90
- ### 压缩策略
94
+ | 层级 | 名称 | 输入 | 输出 | 压缩比 | 触发时机 |
95
+ |------|------|------|------|--------|----------|
96
+ | **T1** | 捕获 | 原始对话 | 详细摘要 | ~45× | 上下文超过 `maxContextLimit` |
97
+ | **T2** | 蒸馏 | T1 摘要(≥ `nudgeGrowthTokens`) | 精炼的决策/结果 | ~10× | T1 摘要累积超过阈值 |
98
+ | **T3** | 浓缩 | T2 摘要(≥ `nudgeGrowthTokens`) | 纯事实(每块 1-3 条) | ~5× | T2 摘要累积超过阈值 |
99
+
100
+ **触发机制:**
101
+
102
+ - **T1** 在原始上下文超过配置限制时触发。模型看到可压缩范围,编写详细摘要,保留文件路径、函数签名、决策和理由。
103
+ - **T2** 在 T1 摘要 token 达到 `nudgeGrowthTokens`(默认上下文窗口的 5%)时触发。模型蒸馏旧的 T1 块 — 保留决策和结果,丢弃冗长的过程细节。
104
+ - **T3** 在 T2 摘要 token 达到同样阈值时触发。模型浓缩为纯事实(已发布的版本、关键 bug、架构决策)。
105
+
106
+ 每层有**独立的节奏计数器** — T2 触发不阻塞 T3。T1 通过 `!shouldInject` 守卫获得优先级:如果 T1 触发了,T2/T3 等到下一轮。这确保原始上下文压缩优先发生(影响最大)。
107
+
108
+ 模型对所有层级使用**同一个 `compress` 工具**。T2/T3 压缩使用块 ID 作为边界(`compress({ content: [{ startId: "b5", endId: "b20", summary: "..." }] })`)。层级根据被消费的块自动检测。
109
+
110
+ **会话容量** — 一个会话从空 → T1 → T2 → T3 → 上下文极限,总共可以处理多少 token(真实校准:500 次 API 调用/天,~9.6K 新 token/调用,T1=45x/T2=10x/T3=3x):
111
+
112
+ | 上下文上限 | 1 个月 | 3 个月 | 到极限 | 极限时间 |
113
+ |-----------|--------|--------|--------|---------|
114
+ | 1M | 19 亿 tok | 105 亿 tok | **689 亿 tok** | 第 259 天(~8.6 月) |
115
+ | 400K | 19 亿 tok | 103 亿 tok | **103 亿 tok** | 第 89 天(~3 月) |
116
+ | 400K(200 调用/天) | 5.6 亿 tok | 25 亿 tok | **95 亿 tok** | 第 212 天(~7 月) |
117
+
118
+ **Token 节省** — 无 ACP 时上下文无限增长,约 100 次 API 调用后崩溃(~0.2 天)。有 ACP 时上下文被压缩在有界范围:
119
+
120
+ | 指标 | 无 ACP | 有 ACP(1M 模型) |
121
+ |------|--------|-----------------|
122
+ | 会话寿命 | ~0.2 天 | 259 天(**长 1295 倍**) |
123
+ | 总 token 产出 | ~5200 万 | 689 亿(**多 1325 倍**) |
124
+
125
+ 核心价值:ACP 不是减少每次调用的 token 成本,而是**让一个会话能处理 1000 倍以上的工作量**。
126
+
127
+ 模型对所有层级使用**同一个 `compress` 工具**。T2/T3 压缩使用块 ID 作为边界(`compress({ content: [{ startId: "b5", endId: "b20", summary: "..." }] })`)。层级根据被消费的块自动检测。
128
+
129
+ ### 压缩策略(一级压缩 / Tier 1)
91
130
 
92
131
  系统会注入一段 prompt,告诉模型当前的上下文比例、压缩比例、上下文是否空闲,以及压缩建议。当触发比例被命中时,内容按**优先级顺序**被压缩:
93
132
 
@@ -107,7 +146,7 @@ stateDiagram-v2
107
146
 
108
147
  ### GC 兜底
109
148
 
110
- 当上下文达到 100% 时,系统自动截断老年代 block 摘要,防止上下文溢出。这是最后的兜底机制,不影响模型的正常压缩/解压操作。
149
+ 当上下文达到 100% 时,系统自动截断老年代 block 摘要,防止上下文溢出。这是最后的兜底机制 — 有了三级压缩,GC 极少激活,因为 T2/T3 蒸馏将摘要开销控制在有界范围内。
111
150
 
112
151
  ### 质量门控(非阻塞,默认关闭)
113
152
 
@@ -443,6 +482,14 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
443
482
 
444
483
  ## 更新日志
445
484
 
485
+ ### v1.14.0 — 三级压缩 + 保留近期消息 + 摘要可见性修复(PR #200, #201, #202)
486
+
487
+ **问题**:长会话稳定性三个关键问题。(1) **摘要累积**(PR #200):摘要块无限累积(v1.13.5+ 强制保护后),以每天约 7.3K token 增长,会话在约 3 天内触及 100K 摘要上限。92.5% 的旧块是已发布/历史工作,零可操作价值。(2) **活跃任务丢失**(PR #201):`lastSegmentSoftBlock` 仅保护最后 1 条消息不被压缩。当模型压缩包含当前任务上下文的范围时,活跃工作丢失——推荐列表本身可能指向应该被保护的消息。(3) **summaryBuffer 过度计数**(PR #202):`getActiveSummaryTokenUsage()` 计算了所有活跃块(如 448 块 = 151K token),但只有约 26 块的 compress 调用在可见上下文窗口中。虚高的计数导致错误的 T2/T3 触发和误导性的 `acp stats` 输出("摘要 146%")。
488
+
489
+ **修复**:(1) PR #200 — 实现了 **三级 LSM-tree 压缩架构**(T1 捕获 → T2 蒸馏 → T3 压缩)。每层压缩前一层输出,细节递减。独立触发器:每层在其输入摘要达到 `nudgeGrowthTokens` 时触发。T1 通过 `!shouldInject` 守卫具有优先级。从消耗的块自动检测层级。层级感知解压(默认 = 向上一级,`full:true` = 递归到原始消息)。新增 `block.tier` 字段、`getTierTokenUsage()`、`hideConsumedCompressCalls()`、`effectiveCompressedTokens`、`deactivatedByUserDeep` 标志。修复 `syncCompressionBlocks` 在锚点消息滚动出上下文时不再错误停用块(21 个会话中 1137 块被错误停用)。919 测试通过。cc-alg v1.2.1(精确锁定)。会话容量:1M 模型处理 68.9B token 持续 259 天;400K 模型 10.3B 持续 89 天。6 轮双代理审查(所有发现已修复)。(2) PR #201 — 在 `compress` 配置中添加 `preserveRecentMessages`(默认 20)、`preserveRecentTokens`(默认 20000)、`preserveLastUserMessage`(默认 true)。`computeProtectedRawIds`/`computeProtectedRefs` 计算保护区;`excludeProtectedRanges` 过滤推荐列表;`checkProtectedRange` 拒绝压缩受保护消息。当所有范围落在保护区内时自动抑制 nudge。880 测试通过。(3) PR #202 — `getActiveSummaryTokenUsage(state, visibleMessageIds?)` 现在接受可选过滤器。`isContextOverLimits` 和 `handleStatsCommand` 传递 `new Set(messages.map(m => m.info.id))`,因此仅计算 `compressMessageId` 在可见窗口中的块。同样修复应用于 `lib/compress/status.ts` 中的 `collectVisibleMessages`。880 测试通过。
490
+
491
+ 文件:`lib/state/{types,utils,state}.ts`、`lib/compress/{state,pipeline,decompress-logic,decompress,hide-consumed,status}.ts`、`lib/messages/inject/{inject,utils}.ts`、`lib/messages/sync.ts`、`lib/messages/prune.ts`、`lib/commands/{recompress,stats}.ts`、`lib/config.ts`、`lib/config-validation.ts`、`lib/prompts/system.ts`、`dcp.schema.json`。测试:`tests/e2e-tier-{compression,simulation}.test.ts`、`tests/preserve-recent.test.ts`、`tests/summary-buffer-visibility.test.ts`、`tests/acp-status.test.ts`、`tests/decompress-logic.test.ts`、`tests/soft-block.test.ts`。
492
+
446
493
  ### v1.13.9-dev.1 — 移除子代理历史重写(PR #180)
447
494
 
448
495
  **问题**:`injectExtendedSubAgentResults` 在 `experimental.allowSubAgents: true` 时,每次消息变换都会重写父代理历史中的 `<task_result>` 工具输出。`subAgentResultCache` 在每次父↔子会话切换时被清空且从不持久化,导致每次变换都重新获取子代理会话并生成新的历史消息体 —— 使 provider prefix cache 失效(观察到的命中率约 56%,健康水平为 96-98%,prefix 冻结在约 22K tokens)。