opencode-acp 1.10.0 → 1.10.1

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.
package/README.md CHANGED
@@ -44,17 +44,17 @@ an average prompt-cache hit ratio above 85%.** (That average — not per-session
44
44
  is explained in [Impact on Prompt Caching](#impact-on-prompt-caching), where it
45
45
  turns out to save far more tokens than traditional compression.)
46
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%) |
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%) |
58
58
 
59
59
  (Context percentages are of the 1M window.)
60
60
 
@@ -70,9 +70,9 @@ Or add to your opencode config:
70
70
 
71
71
  ```json
72
72
  {
73
- "plugin": {
74
- "opencode-acp": "latest"
75
- }
73
+ "plugin": {
74
+ "opencode-acp": "latest"
75
+ }
76
76
  }
77
77
  ```
78
78
 
@@ -153,16 +153,16 @@ ensures key context information is not lost.
153
153
 
154
154
  ACP provides an `/acp` slash command (also accepts `/dcp` for backward compatibility):
155
155
 
156
- | Command | Description |
157
- |---------|-------------|
158
- | `/acp` | Shows available ACP commands |
159
- | `/acp context` | Token usage breakdown by category (system, user, assistant, tools, etc.) and how much has been saved through pruning |
160
- | `/acp stats` | Cumulative pruning statistics across all sessions |
161
- | `/acp sweep [n]` | Prunes all tools since the last user message. Optional count: `/acp sweep 10` prunes the last 10 tools. Respects `commands.protectedTools` |
162
- | `/acp manual [on\|off]` | Toggle manual mode. When on, the AI will not autonomously use context management tools |
163
- | `/acp compress [focus]` | Trigger a single compress tool execution. Optional focus text directs what content to compress, following the active `compress.mode` |
164
- | `/acp decompress <n>` | Restore a specific active compression by ID. Running without an argument shows available compression IDs, token sizes, and topics |
165
- | `/acp recompress <n>` | Re-apply a user-decompressed compression by ID. Running without an argument shows recompressible IDs, token sizes, and topics |
156
+ | Command | Description |
157
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
158
+ | `/acp` | Shows available ACP commands |
159
+ | `/acp context` | Token usage breakdown by category (system, user, assistant, tools, etc.) and how much has been saved through pruning |
160
+ | `/acp stats` | Cumulative pruning statistics across all sessions |
161
+ | `/acp sweep [n]` | Prunes all tools since the last user message. Optional count: `/acp sweep 10` prunes the last 10 tools. Respects `commands.protectedTools` |
162
+ | `/acp manual [on\|off]` | Toggle manual mode. When on, the AI will not autonomously use context management tools |
163
+ | `/acp compress [focus]` | Trigger a single compress tool execution. Optional focus text directs what content to compress, following the active `compress.mode` |
164
+ | `/acp decompress <n>` | Restore a specific active compression by ID. Running without an argument shows available compression IDs, token sizes, and topics |
165
+ | `/acp recompress <n>` | Re-apply a user-decompressed compression by ID. Running without an argument shows recompressible IDs, token sizes, and topics |
166
166
 
167
167
  ---
168
168
 
@@ -183,9 +183,9 @@ Each level overrides the previous, so project settings take priority over global
183
183
  >
184
184
  > ```jsonc
185
185
  > {
186
- > "compaction": {
187
- > "auto": false
188
- > }
186
+ > "compaction": {
187
+ > "auto": false,
188
+ > },
189
189
  > }
190
190
  > ```
191
191
  >
@@ -350,7 +350,7 @@ By default, these tools are always protected from pruning:
350
350
 
351
351
  The `protectedTools` arrays in `commands` and `strategies` add to this default list.
352
352
 
353
- For the `compress` tool, `compress.protectedTools` ensures specific tool outputs are appended to the compressed summary. By default it includes `task`, `skill`, `todowrite`, `todoread`, and `decompress`.
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`.
354
354
 
355
355
  ---
356
356
 
@@ -381,37 +381,38 @@ ACP auto-migrates config from `dcp.jsonc` to `acp.jsonc` and prompts from `dcp-p
381
381
  ---
382
382
 
383
383
  <details>
384
- <summary><strong>Bug Fixes (38 total)</strong> -- applied on top of DCP v3.1.11</summary>
385
-
386
- | # | Severity | Summary |
387
- |---|----------|---------|
388
- | 1 | CRITICAL | State not persisted across restarts -- messageIds, block deactivation, save errors silently lost |
389
- | 2 | CRITICAL | resetOnCompaction() clears all compression blocks -- undoes all pruning work |
390
- | 3 | CRITICAL | prune silently drops summary -- DATA LOSS when no user message precedes anchor |
391
- | 4 | CRITICAL | getCurrentTokenUsage returns 0 -- prevents nudge from ever triggering |
392
- | 5 | HIGH | loadPruneMessagesState duplicates activeBlockIds + reasoning-strip undefined guard |
393
- | 6 | HIGH | Synthetic summary messages get mNNNN refs but are invisible to boundary lookup |
394
- | 7 | HIGH | State not persisted across restarts -- messageIds, block deactivation, and save errors silently lost |
395
- | 8 | HIGH | isMessageCompacted() inconsistent with compaction summary message handling |
396
- | 9 | HIGH | Compressed block summaries retain stale mNNNN message ID tags -- model copies stale IDs |
397
- | 10 | HIGH | Model uses stale mNNNN IDs from nudges/summaries -- compress fails with "startId not available" |
398
- | 11 | HIGH | Major GC skips legacy blocks without generation field -- oversized blocks never collected |
399
- | 12 | HIGH | Percentage-based thresholds calculated against effective input context instead of full model context window |
400
- | 13 | HIGH | Context window leaks -- compressed messages reappear after /compact |
401
- | 14 | HIGH | Compression notifications write full block summaries to DB -- can reach 150KB+ per notification |
402
- | 15 | HIGH | npm auto-install overwrites fork with upstream package |
403
- | 16 | HIGH | Summary mNNNN refs in compress output -- model copies stale message IDs |
404
- | 17 | HIGH | Synthetic messages not in messageIdToBlockId -- compress fails to find them |
405
- | 18 | HIGH | Compress stops model from responding after compression completes |
406
- | 19 | HIGH | Dynamic block guidance breaks API prefix cache |
407
- | 20 | HIGH | GC never deactivates old blocks -- dead-weight accumulates indefinitely |
408
- | 21 | HIGH | Logger + tokenizer 20-50s per-turn latency (268x slowdown) |
409
- | 22 | HIGH | compress throws hard error on reversed block boundaries -- model gives up |
410
- | 23--34 | MEDIUM | Various fixes for dedup, purge errors, schema validation, hook timing, etc. |
411
- | 35 | HIGH | Aging warnings shown at low context usage (<50%) -- triggers unnecessary compress, wastes tokens |
412
- | 36 | HIGH | Compression summary emitted as a standalone user message before the user's real turn -- model reads its own prior assistant output as user input, causing dialog role confusion / self-Q&A loops |
413
- | 37 | HIGH | Message-transform pipeline runs on OpenCode's hidden title/summary/compaction agent requests -- corrupts the request and shared session state, breaking session title generation |
414
- | 38 | CRITICAL | pruneToolOutputs/pruneToolInputs/pruneToolErrors mutate existing messages in-place -- invalidates LLM prefix cache, causing 89% of fresh input tokens to be wasted on cache-invalidating re-sends |
384
+ <summary><strong>Bug Fixes (39 total)</strong> -- applied on top of DCP v3.1.11</summary>
385
+
386
+ | # | Severity | Summary |
387
+ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
388
+ | 1 | CRITICAL | State not persisted across restarts -- messageIds, block deactivation, save errors silently lost |
389
+ | 2 | CRITICAL | resetOnCompaction() clears all compression blocks -- undoes all pruning work |
390
+ | 3 | CRITICAL | prune silently drops summary -- DATA LOSS when no user message precedes anchor |
391
+ | 4 | CRITICAL | getCurrentTokenUsage returns 0 -- prevents nudge from ever triggering |
392
+ | 5 | HIGH | loadPruneMessagesState duplicates activeBlockIds + reasoning-strip undefined guard |
393
+ | 6 | HIGH | Synthetic summary messages get mNNNN refs but are invisible to boundary lookup |
394
+ | 7 | HIGH | State not persisted across restarts -- messageIds, block deactivation, and save errors silently lost |
395
+ | 8 | HIGH | isMessageCompacted() inconsistent with compaction summary message handling |
396
+ | 9 | HIGH | Compressed block summaries retain stale mNNNN message ID tags -- model copies stale IDs |
397
+ | 10 | HIGH | Model uses stale mNNNN IDs from nudges/summaries -- compress fails with "startId not available" |
398
+ | 11 | HIGH | Major GC skips legacy blocks without generation field -- oversized blocks never collected |
399
+ | 12 | HIGH | Percentage-based thresholds calculated against effective input context instead of full model context window |
400
+ | 13 | HIGH | Context window leaks -- compressed messages reappear after /compact |
401
+ | 14 | HIGH | Compression notifications write full block summaries to DB -- can reach 150KB+ per notification |
402
+ | 15 | HIGH | npm auto-install overwrites fork with upstream package |
403
+ | 16 | HIGH | Summary mNNNN refs in compress output -- model copies stale message IDs |
404
+ | 17 | HIGH | Synthetic messages not in messageIdToBlockId -- compress fails to find them |
405
+ | 18 | HIGH | Compress stops model from responding after compression completes |
406
+ | 19 | HIGH | Dynamic block guidance breaks API prefix cache |
407
+ | 20 | HIGH | GC never deactivates old blocks -- dead-weight accumulates indefinitely |
408
+ | 21 | HIGH | Logger + tokenizer 20-50s per-turn latency (268x slowdown) |
409
+ | 22 | HIGH | compress throws hard error on reversed block boundaries -- model gives up |
410
+ | 23--34 | MEDIUM | Various fixes for dedup, purge errors, schema validation, hook timing, etc. |
411
+ | 35 | HIGH | Aging warnings shown at low context usage (<50%) -- triggers unnecessary compress, wastes tokens |
412
+ | 36 | HIGH | Compression summary emitted as a standalone user message before the user's real turn -- model reads its own prior assistant output as user input, causing dialog role confusion / self-Q&A loops |
413
+ | 37 | HIGH | Message-transform pipeline runs on OpenCode's hidden title/summary/compaction agent requests -- corrupts the request and shared session state, breaking session title generation |
414
+ | 38 | CRITICAL | pruneToolOutputs/pruneToolInputs/pruneToolErrors mutate existing messages in-place -- invalidates LLM prefix cache, causing 89% of fresh input tokens to be wasted on cache-invalidating re-sends |
415
+ | 39 | HIGH | Protected tool outputs (skill/task/todowrite) only soft-protected during compression -- appended to summary then pruned from context, losing semantic authority and susceptible to GC truncation. Fixed with hard-exclusion in v1.10.0 |
415
416
 
416
417
  For the complete list with root cause analysis, see the [bug tracker](https://github.com/ranxianglei/opencode-acp/issues).
417
418
 
@@ -421,6 +422,83 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
421
422
 
422
423
  ## Changelog
423
424
 
425
+ ### v1.10.1 — Over-Compression Fix & More Compression Candidates
426
+
427
+ Fixes the over-compression bug reported in issue #18 and GitHub #85, where the `toolOutputReminder` nudge bypassed the adaptive 5%-of-context growth protection and fired ~10x too often on large-context models.
428
+
429
+ #### Over-Compression: toolOutputReminder Bypassed 5% Protection (issue #18, GitHub #85, PR #83)
430
+
431
+ **Problem**: ACP has two independent nudge mechanisms. The main growth nudge correctly uses an adaptive threshold (`nudgeGrowthTokens` = 5% of model context, clamped 6K–50K). But a separate `toolOutputReminder` — added in v1.9.0 to surface accumulated tool outputs — used a **hardcoded 5000-token** tool-growth threshold that fired independently of `minContextLimit` / `maxContextLimit`. On a 1M-context model, 5000 tokens is 0.5% of context, so the reminder fired ~10x more often than intended, emitting a strong "compress these ranges **now**" directive every time. This drove severe over-compression: one investigated session hit only 22.6% peak context yet ran 68 compressions across 499 LLM calls.
432
+
433
+ 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.
434
+
435
+ **Fix** (4 coordinated changes):
436
+ - `lib/messages/inject/inject.ts`: `toolOutputThreshold` now defaults to `nudgeGrowthTokens` (adaptive) instead of the hardcoded `5000`.
437
+ - `lib/config.ts` `mergeCompress`: `toolOutputNudgeThreshold` override now flows through the config merge.
438
+ - `lib/config-validation.ts`: `compress.toolOutputNudgeThreshold` registered as a valid config key.
439
+ - `dcp.schema.json`: `toolOutputNudgeThreshold` property added to the schema.
440
+
441
+ Tests: 3 behavior tests (no-fire on small growth, fire on large growth, override respected) + 1 config-validation test in `tests/inject.test.ts` / `tests/config-validation.test.ts`.
442
+
443
+ #### Persist modelContextLimit Across Restart (issue #18, PR #83)
444
+
445
+ **Problem**: `state.modelContextLimit` was runtime-only — set by the system-prompt hook (which runs after the message-transform hook), so on the first turn after restart it was undefined, causing the adaptive thresholds to fall back to the 6000-token floor instead of the correct value (e.g. 50K for a 1M model). This partially reintroduced the over-compression on the first turn after every restart.
446
+
447
+ **Fix**: `modelContextLimit` is now persisted to the state JSON and restored on load. A guard handles old state files without the field (backward-compatible). The system-prompt hook still refreshes it with the live model value every turn, so a stale persisted value self-corrects within one turn after a model switch.
448
+
449
+ Tests: 2 persistence tests (save+reload round-trip, backward-compat with old files) in `tests/inject.test.ts`.
450
+
451
+ #### Systemic Regression Guard (issue #18, PR #83)
452
+
453
+ A regression test that asserts the **invariant**: the same tool-token growth fires the reminder on a small-context model (200K → 10K threshold) but does NOT fire on a large-context model (400K → 20K threshold). Any future change that reverts a threshold to a fixed value fails this test immediately.
454
+
455
+ #### Increase Max Compression Candidates 5 → 15 (issue #13, PR #81)
456
+
457
+ The context breakdown and tool-output reminder only showed 5 compression candidates, causing the model to compress too narrowly (1 message per batch). Increased to 15 (`largestRanges`, `largestToolRanges`, `toolOutputReminder topRanges`) so the model sees more candidates at once and can cover larger ranges in a single compress call.
458
+
459
+ ---
460
+
461
+ ### v1.10.0 — Hard-Exclusion, Compression Prompt Rewrite, Suffix & Deploy Fixes
462
+
463
+ This release bundles 7 merged PRs. The headline change is **hard-exclusion of protected tool messages** from compression ranges; the rest are fixes and a prompt rewrite that shipped in the same release window.
464
+
465
+ #### Bug 39 — Hard-Exclusion of Protected Tools from Compression (issue #16, PR #75)
466
+
467
+ **Problem**: Protected tool messages (`skill`, `task`, `todowrite`, etc.) were only _soft-protected_ during compression. When the model called `compress` on a range that included a skill output, the original message was pruned from visible context and its content was appended to the summary block. This caused two problems:
468
+
469
+ 1. **Semantic loss**: The skill content became historical recap metadata (`[ACP SYSTEM METADATA — recap...]`), not a live instruction. The model read it as a past artifact, not as active guidance.
470
+ 2. **Data loss via GC**: When the block was promoted to old-gen and the summary exceeded `maxOldGenSummaryLength` (3000 chars), `runTruncateGC` truncated the entire summary — including the appended skill content. Skill outputs (often 2–10 KB) were silently destroyed.
471
+
472
+ **Fix**: Protected tool messages are now **hard-excluded** from compression ranges. When the model calls `compress(startId, endId)` on a range that contains protected tool outputs, those messages are filtered out of the selection _before_ `applyCompressionState` runs. The protected messages survive intact in visible context; only the surrounding non-protected messages are compressed.
473
+
474
+ The filter runs in both range mode (`lib/compress/range.ts`) and message mode (`lib/compress/message.ts`). It uses the existing `compress.protectedTools` config (default: `task`, `skill`, `todowrite`, `todoread`, `decompress`) and the same `isToolNameProtected` matcher used elsewhere.
475
+
476
+ **Verification**: Live-tested by loading the `git-master` skill, then compressing a range spanning the skill output. The skill message (m00170) survived compression; only 15 of 22 messages in the range were compressed (7 protected messages correctly excluded). Tests: 29 dedicated tests in `tests/compress-protected-exclusion.test.ts`.
477
+
478
+ **Compatibility**: No config changes, no persisted-state schema changes. The existing `appendProtectedTools` soft-protection logic is retained as a fallback for any edge case the filter misses.
479
+
480
+ #### Compression Format Prompt Rewrite (issue #13, PR #72)
481
+
482
+ The `compress` tool's summary-format guidance said "EXHAUSTIVE" in the header but then asked for "LEAN" summaries lower down — contradictory directions that left the model unsure how much detail to keep. Replaced with a clear **KEEP / DROP / PRIORITY** taxonomy that maps each rule to a concrete action, eliminating the ambiguity.
483
+
484
+ #### Drop Empty Synthetic User Message from Suffix (issue #12, PR #71)
485
+
486
+ `injectCompressNudges` sometimes forwarded an empty synthetic suffix user message (the carrier for context-status metadata) to the LLM after it had been merged into the preceding block summary. The model then saw an empty user turn with no content. Now spliced out before the forward, plus a backstop `dropEmptyUserMessages` guard.
487
+
488
+ #### Context Transition Notification Arrow Spacing (issue #68, PR #70)
489
+
490
+ `formatContextTransition` in `lib/ui/notification.ts` rendered `141.9K→111K` with no spaces around the `→`. Added explicit spacing for readability: `141.9K → 111K`.
491
+
492
+ #### Route Placeholder Diagnostic to Logger (issue #67, PR #69)
493
+
494
+ `validateSummaryPlaceholders` in `lib/compress/range-utils.ts` used `console.warn` to surface placeholder mismatches, which leaked to stderr and was rendered inline in the chat dialog. Routed the diagnostic through the plugin logger instead so it lands in the ACP debug log without polluting chat.
495
+
496
+ #### Dev-Deploy Legacy Path Sync (issue #9, PR #64)
497
+
498
+ The stale install at the legacy resolution path `~/.cache/opencode/node_modules/opencode-acp/` shadowed the `@latest` deploy at `~/.cache/opencode/packages/opencode-acp@latest/`. `scripts/dev-deploy.sh` now syncs both paths so a stale legacy copy can't override the freshly built bundle.
499
+
500
+ ---
501
+
424
502
  ### v1.9.2 — Persist Nudge Baseline Across Restart (bug #60)
425
503
 
426
504
  **Problem**: After a per-message nudge fired purely on token growth (no compress/decompress around it), the updated `lastPerMessageNudgeTokens` baseline was written to in-memory state but **not persisted to disk** — `saveSessionState()` only ran when `anchorsChanged` was true, and a growth nudge does not always change anchors (turn/iteration anchor sets saturate once seeded, or the last message is an assistant turn with no user turn to anchor). After an OpenCode restart, the stale baseline was reloaded, so `growth = currentTokens − staleBaseline` exceeded the threshold again → the nudge refired on **every single turn** for the rest of the session.
@@ -434,7 +512,7 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
434
512
  }
435
513
  ```
436
514
 
437
- After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/opencode/storage/plugin/acp/{sessionId}.json` on every nudge, so a restart computes growth against the real post-nudge baseline and the nudge only refires when *actual* new growth exceeds `nudgeGrowthTokens`. Regression test added in `tests/inject.test.ts` (seeds a stale baseline to disk, fires a growth nudge with `anchorsChanged=false`, reloads, asserts the persisted baseline advanced).
515
+ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/opencode/storage/plugin/acp/{sessionId}.json` on every nudge, so a restart computes growth against the real post-nudge baseline and the nudge only refires when _actual_ new growth exceeds `nudgeGrowthTokens`. Regression test added in `tests/inject.test.ts` (seeds a stale baseline to disk, fires a growth nudge with `anchorsChanged=false`, reloads, asserts the persisted baseline advanced).
438
516
 
439
517
  **Compatibility**: No schema changes. Existing persisted state loads unchanged. Users hitting #60 should upgrade and the every-turn nudge loop stops on the first nudge after restart.
440
518
 
@@ -442,11 +520,11 @@ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/
442
520
 
443
521
  ### v1.9.1 — Disjoint Visible-Range Segments & Nudge Wording (issue #9 root cause)
444
522
 
445
- **Problem**: Even after v1.9.0, the model kept calling `compress` against IDs that a prior block had consumed. The root cause was that the suffix advertised a single contiguous span "first visible → last visible" that **straddled compression holes** — so the model's first guess for an `endId` landed inside an already-summarized range. Separately, the suffix's `(+X tokens since last nudge)` growth line was being misread as an *overflow* warning, triggering panic compressions of large-but-still-needed ranges.
523
+ **Problem**: Even after v1.9.0, the model kept calling `compress` against IDs that a prior block had consumed. The root cause was that the suffix advertised a single contiguous span "first visible → last visible" that **straddled compression holes** — so the model's first guess for an `endId` landed inside an already-summarized range. Separately, the suffix's `(+X tokens since last nudge)` growth line was being misread as an _overflow_ warning, triggering panic compressions of large-but-still-needed ranges.
446
524
 
447
525
  **Fix 1 — disjoint visible-id segments** (PR #57): `injectVisibleIdRange` no longer emits one "first-to-last" span. It builds the actual surviving segments in ascending ref order and truncates to the largest tool-bearing / high-token segments when the count overflows (`compress.maxVisibleSegments`, default `50`, now plumbed through config defaults + merge + validation + schema). The suffix now reads e.g. `[Visible (top 2 of 3 segments, 803 msgs): m00001–m00929, m00944–m00950 | +1 smaller segment (~1.2K tokens, 6 msgs) omitted]`, so the model sees exactly which ranges are compressible and never targets a hole. The formatting logic is extracted into pure, exported, unit-tested functions (`buildVisibleSegments`, `formatVisibleGuidance`).
448
526
 
449
- **Fix 2 — nudge wording** (PR #58): The incremental-compression guidance line (`💡 Compress incrementally: target the ranges above...`) moved to *after* the largest-ranges list and is reworded to stress that **size alone is not a reason to compress** — a large range that is still needed in full must be kept. Soft efficiency nudges (`growth` / `minLimit` variants) are now prefixed with an explicit *"This is an efficiency nudge to compress early and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full."* so the growth delta isn't mistaken for an overflow alarm. The `maxLimit` path keeps its stronger alert and is intentionally excluded from the efficiency framing.
527
+ **Fix 2 — nudge wording** (PR #58): The incremental-compression guidance line (`💡 Compress incrementally: target the ranges above...`) moved to _after_ the largest-ranges list and is reworded to stress that **size alone is not a reason to compress** — a large range that is still needed in full must be kept. Soft efficiency nudges (`growth` / `minLimit` variants) are now prefixed with an explicit _"This is an efficiency nudge to compress early and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full."_ so the growth delta isn't mistaken for an overflow alarm. The `maxLimit` path keeps its stronger alert and is intentionally excluded from the efficiency framing.
450
528
 
451
529
  **Compatibility**: No persisted-state schema changes. New optional config field `compress.maxVisibleSegments` (number, default `50`); old configs keep working.
452
530
 
@@ -454,9 +532,10 @@ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/
454
532
 
455
533
  ### v1.9.0 — Visible-Range Guidance & Compression Failure Recovery
456
534
 
457
- **Problem**: On large-context models (1M+) the model repeatedly called `compress(startId=m00930, endId=m00943)` against IDs that a prior block had already consumed. It had no stable view of which `mNNNNN` refs were still compressible, the failure error gave no recovery info, `acp_status` was registered but never mentioned in the prompt, and the suffix nudge reported a bare percentage with no indication of *where* the tokens were actually spent.
535
+ **Problem**: On large-context models (1M+) the model repeatedly called `compress(startId=m00930, endId=m00943)` against IDs that a prior block had already consumed. It had no stable view of which `mNNNNN` refs were still compressible, the failure error gave no recovery info, `acp_status` was registered but never mentioned in the prompt, and the suffix nudge reported a bare percentage with no indication of _where_ the tokens were actually spent.
458
536
 
459
537
  **System prompt rewrite**:
538
+
460
539
  - All four context tools (`compress`, `decompress`, `search_context`, `acp_status`) now listed with a one-line "when to use" hint each.
461
540
  - Explicit compress / do-not-compress scenes replace the imperative "compress obvious waste promptly" wording.
462
541
  - New **CONTEXT BREAKDOWN** section explains the 4-category suffix format (`tool | summaries | code | text`), the largest-range candidates, and the incremental "one large consumed range per call" strategy.
@@ -464,6 +543,7 @@ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/
464
543
  - New **task-phase-end** trigger: when a bug hunt / exploration / research sprint ends, compress the phase's redundant churn while preserving findings, file paths, and decision rationale.
465
544
 
466
545
  **Nudge cadence**:
546
+
467
547
  - Dropped the `contextPct >= 15%` floor entirely. Cadence is now pure 5%-of-limit growth with a first-turn baseline (no more forced nudge on turn 1).
468
548
  - Baseline auto-resets after a significant post-compression token drop, so the next nudge fires on the post-compression level instead of waiting a full growth cycle.
469
549
  - Suffix nudge gains a **3-category composition breakdown** (`tool | summaries | code | text`, no double-counting of code-bearing messages) plus the **largest ranges** in the tool and code categories — concrete compression targets, not a bare percentage.
@@ -473,6 +553,7 @@ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/
473
553
  **Compress failure recovery**: `resolveBoundaryIds` failures now return the current visible range (first/last ref), the active block count, and a pointer to `acp_status`. Out-of-range `endId` guesses (unregistered refs that parse higher than the last visible message) are **clamped** to the last visible message instead of failing; refs that are registered but already consumed still fail with the recovery hint (clamping them would silently recompress summarized content).
474
554
 
475
555
  **Hardening**:
556
+
476
557
  - `maxSummaryLengthHard` default raised `4000 → 8000 → 10000`; the compress-tool schema now sources its display value from config so changes propagate.
477
558
  - Removed the stale `MODEL_CONTEXT_LIMITS` 38-entry fallback table — `modelContextLimit` is now sourced solely from the host SDK's `input.model.limit.context`. Providers that omit the field surface `undefined` immediately rather than getting a distorted percentage from a stale guess.
478
559
  - `.catch()` added to every fire-and-forget `saveSessionState` call; removed an `anchorsChanged`-on-baseline path that triggered a concurrent-save race.
@@ -490,6 +571,7 @@ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/
490
571
  **Fix**: Removed `shouldInjectThisTurn` gate from system prompt hook (`hooks.ts:108-112`). System prompt now always injects every turn. Suffix remains gated at `nudgeGrowthTokens` frequency.
491
572
 
492
573
  **Current behavior**:
574
+
493
575
  - **System prompt** (compression philosophy, tool awareness): ✅ every turn
494
576
  - **Suffix** (context level, block list, Tips): gated at nudgeGrowthTokens frequency
495
577
 
@@ -500,31 +582,37 @@ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/
500
582
  **Problem**: Large-context models (1M+) over-compressed at 20-30% context because Tips fired every 6K tokens (0.6% of 1M). System prompt injected every turn added constant pressure.
501
583
 
502
584
  **Adaptive nudgeGrowthTokens**:
585
+
503
586
  - Default is now adaptive: 5% of `modelContextLimit`, clamped to [6000, 50000]
504
- - 128K → 6.4K, 200K → 10K, 500K → 25K, 1M → 50K, 2M+ → 50K (cap)
587
+ - 128K → 6.4K, 200K → 10K, 500K → 25K, 1M → 50K, 2M+ → 50K (cap)
505
588
  - Users can still set explicit `nudgeGrowthTokens` to override
506
589
  - Removed hardcoded `6000` from schema defaults (was shadowing adaptive logic)
507
590
 
508
591
  **System prompt gating**:
592
+
509
593
  - SYSTEM prompt + `<dcp-system-reminder>` tags now pulse at `nudgeGrowthTokens` frequency
510
594
  - Between nudges: system prompt injects **nothing** — zero compression noise
511
595
  - First turn (`undefined` sentinel): always injects (establishes baseline)
512
596
 
513
597
  **New tool: `acp_status`**:
598
+
514
599
  - On-demand inspection of all compressed blocks (ID, tokens, age, topic)
515
600
  - Replaces verbose block list in suffix with one-liner: `Compressed blocks: N (XK summary, last Ym ago). Use acp_status for details.`
516
601
 
517
602
  **Compress notification improvement**:
603
+
518
604
  - Header shows context before→after: `▣ ACP | Context 251.2K→249.3K`
519
605
  - No percentage or limit shown (prevents model from anchoring on ceiling)
520
606
 
521
607
  **Bug fixes**:
608
+
522
609
  - `lastPerMessageNudgeTokens` reset to `0` after compress bypassed growth gate (feedback loop)
523
610
  - Schema default `6000` shadowed `resolveAdaptiveNudgeGrowth()` — adaptive never activated
524
611
  - `applyAnchoredNudges` + `injectContextUsage` duplicated context usage text
525
612
  - `lastNudgeTokens === 0` sentinel replaced with `undefined` (explicit "never nudged")
526
613
 
527
614
  **Tooling**:
615
+
528
616
  - `scripts/dev-deploy.sh` — one-command build + deploy (auto-detects node, typecheck, build, deploy)
529
617
  - Post-compress state transition integration tests (3 new)
530
618
  - `acp_status` dedicated tests (7 new)
@@ -533,24 +621,28 @@ After this, `lastPerMessageNudgeTokens` is correctly flushed to `~/.local/share/
533
621
 
534
622
  ### v1.8.0 — Principle-Driven Prompts
535
623
 
536
- **Philosophy**: Replaced verbose context-management guidance with 4 concise principles injected every turn. The model now sees *what matters* (principles) instead of *what to do* (rigid rules).
624
+ **Philosophy**: Replaced verbose context-management guidance with 4 concise principles injected every turn. The model now sees _what matters_ (principles) instead of _what to do_ (rigid rules).
537
625
 
538
626
  **Prompt changes**:
627
+
539
628
  - 4 principles replace CONTEXT PRESSURE LEVELS, 7-item priority list, DO NOT RE-COMPRESS rules
540
629
  - Context display simplified: absolute token count only, no percentage
541
630
  - `<acp-context>` tag wrapping (backward compatible with `<dcp-context>`)
542
631
 
543
632
  **Hybrid Tips frequency**:
633
+
544
634
  - 💡 Light Tips (15-45%): Every turn — non-disruptive reminder
545
635
  - ⚠️ Warning Tips (45%+): Key nodes only — first crossing or 10pp growth, prevents over-compression
546
636
 
547
637
  **Config simplification**:
638
+
548
639
  - Removed `hardNudgeContextPercent` — merged into `minContextLimit`/`maxContextLimit`
549
640
  - Removed `perMessageNudgeGrowthPercent` — light Tips show every turn
550
641
  - `maxSummaryLength` default: 200 → 2000
551
642
  - `maxSummaryLengthHard` default: 3000 → 4000
552
643
 
553
644
  **Bug fixes**:
645
+
554
646
  - Windows path validation: `os.tmpdir()` + `path.relative()` (was hardcoded `/tmp/`)
555
647
  - Compress after-detection: reset warning tracking
556
648
  - Dead code cleanup: `shouldInjectPerMessageNudge`, no-op template