Skip to content

Commit d371b63

Browse files
feat: 3-tier compression (LSM tree architecture) (#200)
* feat(state): add CompressionTier type, tier field, and getTierTokenUsage Foundation for multi-tier compression: CompressionTier (1|2|3) type, tier field on CompressionBlock, lastTierNudgeTokens in Nudges, getTierTokenUsage() utility, and tier parsing in loadPruneMessagesState. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(compress): tier auto-detection in applyCompressionState When compressing blocks (b prefix), auto-detect output tier from consumed blocks: max(consumed.tier) + 1, capped at 3. Pipeline phantom-check carve-out for tier escalation (consumedBlockIds >= 2). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(compress): hide consumed compress calls from visible context New hideConsumedCompressCalls module removes tool-call parts from compress calls whose blocks have been consumed by tier 2+ compression. Prevents double-counting: consumed block summaries no longer appear both in the tier-1 compress call AND the tier-2 compress call. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(inject): independent tier 2/3 trigger in nudge system Each tier triggers INDEPENDENTLY when its input summaries reach nudgeGrowthTokens. T2 fires when tier1Tokens >= threshold, T3 fires when tier2Tokens >= threshold. Compression ratio naturally controls frequency: T1 every few turns, T2 every few dozen, T3 every few hundred. Includes growth-floor cadence gate and clears lastTierNudgeTokens on compress. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(status): tier labels and effective token display in acp_status acp_status now shows tier labels (T1/T2/T3), effective compressed tokens (recursive sum through consumed blocks), and tier breakdown line. System prompt gains MULTI-TIER COMPRESSION section explaining the 3-tier mechanism to the model. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test: tier compression E2E tests 9 E2E tests covering: tier auto-detection (T1 from raw, T2 from T1 blocks, T3 from T2 blocks, cap at 3), tier token usage counting, and trigger threshold logic. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore: pin context-compress-algorithms to 1.2.0 Pin exact version instead of ^1.0.0 to prevent incompatible versions. cc-alg 1.2.0 adds TIER2_DISTILL_RULES and TIER3_CONDENSE_RULES required by the tier compression system. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore: add tier lifecycle simulation script scripts/simulate-tier-lifecycle.ts: 5-year simulation of tier compression lifecycle from empty session. Shows T1/T2/T3 trigger frequency, compression ratios, and net context growth rate. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: tier compression devlog REQ + WORKLOG for 3-tier compression (LSM tree architecture): problem analysis, design decisions, implementation phases, Oracle review fixes, trigger redesign, cc-alg release. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: address dual-agent review findings on tier compression Fixes from PR #200 dual-agent review: - Remove unsound (cbTier+1) as CompressionTier cast; use Math.min(3, maxConsumedTier+1) - Fix enoughCandidates dead code: candidates.length >= 2 (not tautological token check) - Remove unnecessary structural cast in hide-consumed.ts (p as {tool?:string}).tool Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test: fix makeAssistantMessage 3-arg call + add tier persistence round-trip tests Fixes from PR #200 dual-agent review: - makeAssistantMessage now accepts optional extraParts (3rd arg was silently discarded before) - Add 3 persistence round-trip tests: tier 1/2/3 preserved, missing tier defaults to undefined, invalid tier (>3) rejected Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(compress): prevent cross-tier contamination and track effectiveCompressedTokens Two deferred known limitations from dual-agent review: 1. Cross-tier contamination: when a tier escalation nudge suggests 'compress(b5, b20)', search.ts resolves the range by anchor position and consumes ALL active blocks in range — including non-target-tier blocks. This causes unintended tier escalation (e.g., T2 trigger consuming a T2 block → output T3 instead of T2). Fix: nudge sorts candidates by blockId and narrows the suggested range to exclude non-target active blocks (inject.ts). Safety net in applyCompressionState uses minConsumedTier for output tier and skips deactivation of non-target-tier consumed blocks (state.ts). 2. compressedTokens=0 for T2+ blocks: tier escalation blocks have no direct messages, so compressedTokens=0. This causes notification '0→2K', stats undercount, and misleading display. Fix: new effectiveCompressedTokens field on CompressionBlock (types.ts). Computed at creation time as compressedTokens + sum of consumed blocks' effectiveCompressedTokens (state.ts). Persisted through loadPruneMessagesState (utils.ts). Stats use effective tokens. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(ui): use effectiveCompressedTokens in notification and display surfaces notification.ts: log compression uses effective tokens instead of raw compressedTokens (was 0 for T2+ blocks). status.ts: getEffectiveCompressedTokens prefers stored field, falls back to recursive computation for old state files. compression-targets.ts: sum uses effectiveCompressedTokens ?? compressedTokens so decompress/recompress display labels show full coverage. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test: add cross-tier safety and effectiveCompressedTokens tests 4 new E2E tests: - T2 trigger narrows range when non-target (T2) block between T1 candidates - applyCompressionState mixed-tier consumption produces minTier+1 - T2 block gets effectiveCompressedTokens = consumed T1 tokens - T1 block gets effectiveCompressedTokens = compressedTokens 2 new persistence tests: - effectiveCompressedTokens round-trip through loadPruneMessagesState - missing effectiveCompressedTokens defaults to undefined Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: update devlog with deferred limitation fixes Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * refactor: remove dead code from cross-tier fix (maxConsumedTier, included var) Cleanup from dual-agent review: - Remove unused maxConsumedTier variable (output tier uses minConsumedTier) - Remove unused included variable (includedBlockIds uses consumed.filter directly) - Fix indentation on includedBlockIds/consumedBlockIds fields Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore: pin context-compress-algorithms to 1.2.1 TIER2/TIER3 prompt fixes: remove Memory reference, fix SIZE TARGET contradictions, add priority guidance, add cross-block synthesis. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * refactor(state): split lastTierNudgeTokens into independent per-tier counters T2 and T3 triggers now have independent cadence: lastTier2NudgeTokens and lastTier3NudgeTokens. Old lastTierNudgeTokens kept as @deprecated persisted field, migrated to lastTier2NudgeTokens on load (same tier level). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * refactor(inject): unified tier trigger loop with T1 priority Replace separate T2/T3 trigger block with a unified loop over tierChecks array. T1 (normal compression) gets priority via !shouldInject guard — if T1 fires, tier escalation is skipped this turn. Each tier has independent cadence counter, so T2 firing doesn't block T3. First match in priority order wins, then break. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test: large-scale E2E simulation for tier compression strategy (8 tests) SIM 1-8 verify: T1 fires on context limit, T2 escalation from T1 summaries, T3 escalation from T2 summaries, T1 priority over T2, independent per-tier cadence, cross-tier safety narrowing, no-crash steady state over 30 turns. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs(readme): document three-tier compression architecture (T1/T2/T3) Replace old single-tier compress/decompress diagram with three-tier LSM-tree lifecycle: T1 (capture, ~45x) -> T2 (distill, ~10x) -> T3 (condense, ~5x). Add tier trigger explanation (independent cadence counters, T1 priority guard), 5-year lifecycle projection table, and rename 'Compression strategy' to 'Compression strategy (Tier 1)'. Update GC safety net section to note 179 tok/day net growth. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs(readme): add token estimates to lifecycle projection table Show summary token counts at year 5 (T1=87K, T2=9K, total~96K=9.6% of 1M) and at 15-year fill point (~150K). Makes the projection concrete. * docs(readme): fix lifecycle projection with actual simulation data Year 5: visible context 327K (32.7%), not 96K. Summary overhead 64K. Net growth 179 tok/day. Context fills 1M in ~15 years. Simulation source: scripts/simulate-tier-lifecycle.ts * docs: add cumulative token savings table to lifecycle projection Track cumulative token consumption (billing metric) alongside instantaneous context size. Key finding: without ACP cumulative grows O(n²), with ACP O(n). 1-year session saves 96%, 5-year saves 97%. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: replace lifecycle projection with real-calibrated session capacity data Old projection used 7.3K/day growth (wrong). New simulation calibrated from real sessions (500 calls/day, 9.6K/call). Two context limits: - 1M model: 68.9B tokens over 259 days - 400K model: 10.3B tokens over 89 days (500 calls/day) - 400K model: 9.5B tokens over 212 days (200 calls/day) Key insight: ACP enables 1000x+ more total work per session. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(decompress): tier-aware decompress — one level up by default Decompressing a T2+ block now restores the PREVIOUS tier's summaries (e.g., decompress T2 → T1 summaries visible) instead of jumping all the way to original messages. This prevents catastrophic context explosion when decompressing high-tier blocks. New option: full:true restores all content to original messages (legacy behavior). Useful when exact original content is needed. Changes: - deactivateCompressionTarget: consumed blocks only marked deactivatedByUser when full:true (was always) - decompress tool: added full parameter to schema - system prompt: updated decompress description for tier awareness - Tests: 2 new unit tests + 2 new E2E tests (915 total pass) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: address dual-agent review findings (5 MEDIUM) 1. full:true decompress now recursively walks consumed tree (T3+ fix) 2. stats use compressedTokens not effectiveCompressedTokens (no double-count) 3. Fix broken SIM 1 assertion + remove as any cast 4. minConsumedTier default 3→1 (safe fallback for missing blocks) 5. Remove @ts-expect-error on optional tier field 6. Replace (p: any) with proper type guards in test helpers 915 tests pass, typecheck clean. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test: add E2E round-trip tests for compress→decompress→recompress 4 new E2E tests (919 total pass): 1. Round-trip content identity: compress→decompress→content identical 2. Recompress no redundancy: decompress→recompress→no duplicate blocks 3. T3 default decompress: T3→T2 summaries (one level up) 4. T3 full:true decompress: recursive to raw (all descendants deactivated) Also reverts minConsumedTier default 3→1 (M4 fix was incorrect — min logic requires high initial value; starting at 1 breaks T3 detection when consuming T2 blocks). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: blocks survive anchor message removal (sync.ts) Removed anchor-missing deactivation in syncCompressionBlocks. Previously, blocks were deactivated when their anchorMessageId (the compress tool call) was removed from the message list. This caused 1137 blocks across 21 sessions to be incorrectly deactivated when opencode compaction removed old compress tool calls. The block's existence IS proof that compression happened — same logic as Bug 3 (compressMessageId check, already removed). Updated 4 tests: blocks now stay active when anchor is gone. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(state): tier detection + effectiveMessageIds filter (M1+M2) M1: minConsumedTier uses undefined sentinel instead of magic init=3. If consumed IDs don't resolve in blocksById, falls back to T1 (safe default). M2: effectiveMessageIds now filtered by targetTierForConsumption. Non-target- tier consumed blocks no longer pollute the effective set. Also: includedBlockIds stores ALL consumed (unfiltered) for display, while consumedBlockIds remains tier-filtered for sync deactivation. * fix(decompress): deactivatedByUserDeep for full:true recursive (M4+L1) M4: full:true decompress now sets deactivatedByUserDeep (not deactivatedByUser) on consumed blocks. Recompress walks consumedBlockIds recursively to clear it. This prevents consumed blocks from being permanently flagged after full:true. L1: Removed missingOriginBlockIds dead code from sync.ts (declared, never populated, only logged). Also: sync.ts and hide-consumed.ts check both deactivatedByUser and deactivatedByUserDeep to keep blocks inactive. * fix(pipeline+status): phantom tier check + includedBlockIds display (M3+L2) M3: Phantom carve-out now requires homogeneous tier (tiers.size === 1). Mixed-tier consumed blocks (cross-tier contamination) no longer bypass phantom rejection. L2: Status display uses includedBlockIds (all consumed) instead of consumedBlockIds (target-tier only) for nested= rendering. * test: fix review findings M5/M6 + update assertions for M4 M5: Renamed misleading test 'fires even when T1 nudge would also fire' to 'fires when T1 summaries exceed threshold even with large context'. Code has T1 priority via !shouldInject guard, not independent firing. M6: Fixed simulation factory parentBlockIds from [...consumedBlockIds] (reversed semantics) to [] (blocks aren't consumed by anything in test). Updated assertions for M4: consumed blocks now check deactivatedByUserDeep instead of deactivatedByUser. * docs: update devlog REQ + WORKLOG with all phases (7-10) --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent df83bc8 commit d371b63

32 files changed

Lines changed: 3265 additions & 102 deletions

‎README.md‎

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,23 +90,76 @@ Or add to your opencode config:
9090
ACP hands the context-compression tool directly to the model. The model is
9191
**100% responsible** for context compression. The model's primary tools are
9292
**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.
9597

96-
### Lifecycle
98+
### Lifecycle — Three-Tier Compression
9799

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:
101103

102104
```mermaid
103105
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
107113
```
108114

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)
110163

111164
The system injects a prompt telling the model the current context ratio, the
112165
compression ratio, whether context is idle, and compression suggestions. When the
@@ -132,7 +185,7 @@ later work.
132185

133186
### GC safety net
134187

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.
136189

137190
### Quality gate (non-blocking, off by default)
138191

‎README.zh-CN.md‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,59 @@ opencode plugin opencode-acp@latest --global
7474

7575
## 工作原理
7676

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 截断作为兜底。
7878

79-
### 生命周期
79+
### 生命周期 — 三级压缩
8080

81-
两个操作:**压缩**、**解压缩**。内容在原始与压缩之间循环。当上下文达到 100% 时,GC 自动截断老年代 block 作为兜底:
81+
ACP 采用**三级 LSM-tree 压缩架构**,灵感来自数据库存储引擎。每一级压缩上一级的输出,产生逐渐精炼的摘要,频率自然递减:
8282

8383
```mermaid
8484
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% 上下文)
8892
```
8993

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)
91130

92131
系统会注入一段 prompt,告诉模型当前的上下文比例、压缩比例、上下文是否空闲,以及压缩建议。当触发比例被命中时,内容按**优先级顺序**被压缩:
93132

@@ -107,7 +146,7 @@ stateDiagram-v2
107146

108147
### GC 兜底
109148

110-
当上下文达到 100% 时,系统自动截断老年代 block 摘要,防止上下文溢出。这是最后的兜底机制,不影响模型的正常压缩/解压操作。
149+
当上下文达到 100% 时,系统自动截断老年代 block 摘要,防止上下文溢出。这是最后的兜底机制 — 有了三级压缩,GC 极少激活,因为 T2/T3 蒸馏将摘要开销控制在有界范围内。
111150

112151
### 质量门控(非阻塞,默认关闭)
113152

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# 3-Tier Compression (LSM Tree Architecture)
2+
3+
## Problem
4+
Summary blocks accumulate 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 are shipped/historical work with zero actionable value.
5+
6+
## Solution
7+
Implement a 3-tier LSM Tree compression architecture:
8+
- **Tier 1** (default): Full-detail compression of conversation ranges (existing behavior)
9+
- **Tier 2**: Distillation of old tier-1 summaries → decisions/outcomes only (~1/12 ratio)
10+
- **Tier 3**: Ultra-condensation of tier-2 summaries → bare facts (~1/3 ratio)
11+
12+
End-to-end: 1/60 × 1/12 × 1/3 = 1/2160. Session longevity: 3 days → 271 days (9 months).
13+
14+
## Design
15+
- Reuse existing `compress` tool for all tiers — `b` prefix (block ID) auto-detects tier
16+
- Each tier uses different prompt rules (from cc-alg v1.2.1)
17+
- `block.tier` field on CompressionBlock tracks tier (1/2/3, undefined=1)
18+
- `applyCompressionState` auto-detects output tier from consumed blocks (min consumed tier + 1)
19+
- Per-tier token counting via `getTierTokenUsage()`
20+
- Independent tier triggers with T1 priority: each tier checks its input summaries against `nudgeGrowthTokens`
21+
- Tier-aware decompress: default = one level up (T2→T1), `full:true` = recursive to raw
22+
- Cross-tier contamination prevention: candidates sorted by blockId, range narrowed when non-target blocks exist
23+
24+
## Scope
25+
- **cc-alg v1.2.1**: TIER2_DISTILL_RULES, TIER3_CONDENSE_RULES prompts (keep function/module refs, source headers)
26+
- **opencode-acp**: block.tier field, tier auto-detection, per-tier counting, independent triggers, tier-aware decompress, hide-consumed, sync.ts anchor-survival fix
27+
28+
## Session Capacity (real-calibrated)
29+
| Context limit | Total tokens | Duration |
30+
|---------------|-------------|----------|
31+
| 1M | 68.9B | 259 days |
32+
| 400K (500 calls/day) | 10.3B | 89 days |
33+
| 400K (200 calls/day) | 9.5B | 212 days |
34+
35+
## Status
36+
33 commits, 919 tests pass, dual-agent reviewed (all findings fixed).

0 commit comments

Comments
 (0)