Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,34 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi

---

## Changelog

### v1.7.0 — Principle-Driven Prompts

**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).

**Prompt changes**:
- 4 principles replace CONTEXT PRESSURE LEVELS, 7-item priority list, DO NOT RE-COMPRESS rules
- Context display simplified: absolute token count only, no percentage
- `<acp-context>` tag wrapping (backward compatible with `<dcp-context>`)

**Hybrid Tips frequency**:
- 💡 Light Tips (15-45%): Every turn — non-disruptive reminder
- ⚠️ Warning Tips (45%+): Key nodes only — first crossing or 10pp growth, prevents over-compression

**Config simplification**:
- Removed `hardNudgeContextPercent` — merged into `minContextLimit`/`maxContextLimit`
- Removed `perMessageNudgeGrowthPercent` — light Tips show every turn
- `maxSummaryLength` default: 200 → 2000
- `maxSummaryLengthHard` default: 3000 → 4000

**Bug fixes**:
- Windows path validation: `os.tmpdir()` + `path.relative()` (was hardcoded `/tmp/`)
- Compress after-detection: reset warning tracking
- Dead code cleanup: `shouldInjectPerMessageNudge`, no-op template

---

## License

AGPL-3.0-or-later -- This project is a fork of [@tarquinen/opencode-dcp](https://github.com/Tarquinen/opencode-dynamic-context-pruning). Original copyright belongs to the original author. Modifications and bug fixes by ranxianglei.
28 changes: 28 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,34 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将

---

## 更新日志

### v1.7.0 — 原则驱动提示

**理念**:用 4 条简洁原则替代冗长的上下文管理指导。模型现在看到的是*重要原则*而非*死板规则*。

**提示变更**:
- 4 条原则替代 CONTEXT PRESSURE LEVELS、7 项优先级列表、DO NOT RE-COMPRESS 规则
- 上下文显示简化:仅显示绝对 token 数,不显示百分比
- `<acp-context>` 标签包裹(向后兼容 `<dcp-context>`)

**混合 Tips 频率**:
- 💡 轻量提示(15-45%):每轮显示 — 不打扰
- ⚠️ 警告提示(45%+):仅关键节点 — 首次跨越或增长 10pp,防止过度压缩

**配置简化**:
- 移除 `hardNudgeContextPercent` — 合并到 `minContextLimit`/`maxContextLimit`
- 移除 `perMessageNudgeGrowthPercent` — 轻量提示每轮显示
- `maxSummaryLength` 默认值:200 → 2000
- `maxSummaryLengthHard` 默认值:3000 → 4000

**Bug 修复**:
- Windows 路径校验:`os.tmpdir()` + `path.relative()`(原硬编码 `/tmp/`)
- 压缩检测后:重置警告追踪
- 死代码清理:`shouldInjectPerMessageNudge`、空操作模板

---

## 许可证

AGPL-3.0-or-later — 本项目是 [@tarquinen/opencode-dcp](https://github.com/Tarquinen/opencode-dynamic-context-pruning) 的分支。原始版权归原始作者所有。修改和错误修复由 ranxianglei 完成。
87 changes: 70 additions & 17 deletions dcp.schema.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/ranxianglei/opencode-acp/master/dcp.schema.json",
"title": "DCP Plugin Configuration",
"description": "Configuration schema for the OpenCode Dynamic Context Pruning plugin",
"title": "ACP Plugin Configuration",
"description": "Configuration schema for the OpenCode Agent Context Pruning plugin",
"type": "object",
"additionalProperties": false,
"properties": {
Expand All @@ -27,13 +27,20 @@
},
"pruneNotification": {
"type": "string",
"enum": ["off", "minimal", "detailed"],
"enum": [
"off",
"minimal",
"detailed"
],
"default": "detailed",
"description": "Level of notification shown when context management occurs"
},
"pruneNotificationType": {
"type": "string",
"enum": ["chat", "toast"],
"enum": [
"chat",
"toast"
],
"default": "chat",
"description": "Where to display notifications (chat message or toast notification)"
},
Expand Down Expand Up @@ -135,13 +142,20 @@
"properties": {
"mode": {
"type": "string",
"enum": ["range", "message"],
"enum": [
"range",
"message"
],
"default": "range",
"description": "Compression mode. 'range' compresses spans into block summaries, 'message' compresses individual raw messages."
},
"permission": {
"type": "string",
"enum": ["ask", "allow", "deny"],
"enum": [
"ask",
"allow",
"deny"
],
"default": "allow",
"description": "Permission mode (deny disables the tool)"
},
Expand Down Expand Up @@ -225,7 +239,10 @@
},
"nudgeForce": {
"type": "string",
"enum": ["strong", "soft"],
"enum": [
"strong",
"soft"
],
"default": "soft",
"description": "Controls how likely compression is after user messages. 'strong' is more likely, 'soft' is less likely."
},
Expand All @@ -246,6 +263,18 @@
"type": "boolean",
"default": false,
"description": "When enabled, your messages are never lost during compression"
},
"minNudgeContextPercent": {
"type": "number",
"default": 15,
"minimum": 0,
"description": "Minimum context usage percent to show per-message nudges"
},
"maxSummaryLengthHard": {
"type": "number",
"default": 4000,
"minimum": 1,
"description": "Hard limit for summary length in characters"
}
},
"default": {
Expand All @@ -260,7 +289,9 @@
"nudgeForce": "soft",
"protectedTools": [],
"protectTags": false,
"protectUserMessages": false
"protectUserMessages": false,
"minNudgeContextPercent": 15,
"maxSummaryLengthHard": 3000
}
},
"gc": {
Expand All @@ -270,7 +301,9 @@
"properties": {
"algorithm": {
"type": "string",
"enum": ["truncate"],
"enum": [
"truncate"
],
"default": "truncate"
},
"promotionThreshold": {
Expand All @@ -288,8 +321,13 @@
"majorGcThresholdPercent": {
"default": "100%",
"oneOf": [
{ "type": "number" },
{ "type": "string", "pattern": "^\\d+(?:\\.\\d+)?%$" }
{
"type": "number"
},
{
"type": "string",
"pattern": "^\\d+(?:\\.\\d+)?%$"
}
]
},
"batchCleanup": {
Expand All @@ -301,24 +339,39 @@
"default": "60%",
"description": "Nudge tier: at/above this usage, a reminder to merge marked blocks is injected.",
"oneOf": [
{ "type": "number" },
{ "type": "string", "pattern": "^\\d+(?:\\.\\d+)?%$" }
{
"type": "number"
},
{
"type": "string",
"pattern": "^\\d+(?:\\.\\d+)?%$"
}
]
},
"highThreshold": {
"default": "75%",
"description": "Auto merge tier: at/above this usage, all marked blocks are merge-compressed into one.",
"oneOf": [
{ "type": "number" },
{ "type": "string", "pattern": "^\\d+(?:\\.\\d+)?%$" }
{
"type": "number"
},
{
"type": "string",
"pattern": "^\\d+(?:\\.\\d+)?%$"
}
]
},
"forceThreshold": {
"default": "90%",
"description": "Force merge tier: at/above this usage, all old-gen blocks are merged regardless of marks.",
"oneOf": [
{ "type": "number" },
{ "type": "string", "pattern": "^\\d+(?:\\.\\d+)?%$" }
{
"type": "number"
},
{
"type": "string",
"pattern": "^\\d+(?:\\.\\d+)?%$"
}
]
}
}
Expand Down
16 changes: 16 additions & 0 deletions devlog/2026-07-01_principle-driven-prompts/REQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Principle-Driven Compression Prompts

## Problem
System prompt had 72 lines of detailed compression rules (7-item priority list, 3 pressure levels, hardcoded thresholds). Models mechanically followed rules regardless of context level — compressing at 6% context, losing critical task details.

## Requirements
- R1: Simplify system prompt to high-level principles (~15 lines)
- R2: Per-message shows only context number (no compression guidance)
- R3: Every 10 percentage points (from 15%): show Tips with tool names (not commands)
- R4: Below 15%: no compression prompts at all
- R5: At 65%+: stronger tone about overflow risk
- R6: Add "BE FRUGAL" section with examples of obvious waste
- R7: Config: minNudgeContextPercent=15, growthPercent=10pp

## Design Philosophy
Minimal intervention. Give smart models principles, not rules. Let them decide when/what to compress.
63 changes: 63 additions & 0 deletions devlog/2026-07-01_principle-driven-prompts/WORKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Worklog: Principle-Driven Compression Prompts

## Session Summary (commits f47e383 → 8d22f7c)

### Core Philosophy Change
Replaced verbose context-management guidance (CONTEXT PRESSURE LEVELS, 7-item priority list, DO NOT RE-COMPRESS rules) with **4 concise principles** injected every turn:

1. All compression serves the primary task, but be frugal.
2. Context capacity is precious — compress waste promptly.
3. Save context by compressing consumed outputs, not by avoiding tools.
4. Compress by need, not by percentage.

### system.ts (72→15 lines)
- Removed: CONTEXT PRESSURE LEVELS, WHAT TO COMPRESS FIRST (7-item list), DO NOT RE-COMPRESS, WHAT TO COMPRESS CAREFULLY, BEFORE/AFTER COMPRESSING
- Added: 4 principle-driven sentences
- Fixed: Tag format `` / `` wrapping

### utils.ts (buildContextUsageGuidance)
- Removed: All guidance text ("Be frugal", "Extract and keep what matters", pressure level descriptions)
- Removed: Percentage display (no longer shows "X%")
- Now returns: Just "Context: XK tokens." + 4 principles

### inject.ts — Hybrid Tips Frequency
- **Light Tips (💡)**: Show every turn when context ≥15% and below minContextLimit
- **Warning Tips (⚠️)**: Show at key nodes only (first crossing threshold or 10pp growth)
- 45-55% (minContextLimit): "⚠️ Context is growing..."
- 55%+ (maxContextLimit): "⚠️ Context limit reached — compress now." + compress call format
- Reset warning tracking when context drops below warning zone
- Removed: `shouldInjectPerMessageNudge` dead function
- Removed: `hardNudgeContextPercent` parameter (merged into minContextLimit/maxContextLimit)
- Removed: `perMessageNudgeGrowthPercent` for light Tips
- Fixed: `usageTag` no-op template `${rawUsage}` → `rawUsage`

### nudge.ts (buildCompressedBlockGuidance)
- Removed: Consolidation suggestion
- Added: Block token counts in list — `b50 (76t), b51 (88t), ...`

### range.ts + message.ts (compress tools)
- Changed: Read `maxSummaryLengthHard` from config instead of hardcoded 3000
- Added: `summaryMaxChars` optional parameter for per-call override

### decompress.ts
- Added: `toFile` optional parameter — writes content to file without inflating context
- Fixed: Windows path validation — `os.tmpdir()` + `path.relative()` instead of hardcoded `/tmp/` + `startsWith(dir+"/")`

### config.ts
- Added: `minNudgeContextPercent` (default 15) — Tips start showing
- Removed: `hardNudgeContextPercent` (replaced by minContextLimit/maxContextLimit)
- Removed: `perMessageNudgeGrowthPercent` (light Tips show every turn)
- Changed: `maxSummaryLength` default 200 → 2000
- Changed: `maxSummaryLengthHard` default 3000 → 4000

### dcp.schema.json
- Removed: `hardNudgeContextPercent`, `perMessageNudgeGrowthPercent`
- Updated: Default values aligned with config.ts

### config-validation.ts
- Removed: Validation blocks for deleted parameters

## Verification
- typecheck: 0 errors
- tests: 496 pass, 0 fail
- build: 330KB
46 changes: 46 additions & 0 deletions lib/compress/decompress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ function buildSchema() {
blockId: tool.schema
.string()
.describe('Block reference to decompress (e.g., "b0", "b2")'),
toFile: tool.schema
.string()
.optional()
.describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Use read tool to access specific parts. Example: '/tmp/block52.txt'"),
}
}

Expand Down Expand Up @@ -121,6 +125,48 @@ export function createDecompressTool(ctx: ToolContext): ReturnType<typeof tool>
return `Error: Block ${target.displayId} is not active. It may have already been decompressed.`
}

if (args.toFile) {
const targetPath = args.toFile as string
const os = await import("os")
const path = await import("path")
const allowedDirs = [
os.tmpdir() + "/",
path.join(os.homedir(), ".cache", "opencode") + "/",
]
const resolved = path.resolve(targetPath)
const isAllowed = allowedDirs.some((dir) => {
const rel = path.relative(dir, resolved)
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
})
if (!isAllowed) {
return `Error: toFile path must be under ${os.tmpdir()} or ~/.cache/opencode/. Got: ${targetPath}`
}
const block = activeBlocks[0]
const msgIds = new Set(block.effectiveMessageIds ?? [])
const blockMessages = rawMessages.filter((m) => {
const id = (m as { id?: string }).id ?? (m as { messageId?: string }).messageId ?? ""
return msgIds.has(id)
})
const lines = blockMessages.map((m) => {
const msg = m as { role?: string; type?: string; content?: unknown; text?: string }
const role = msg.role || msg.type || "unknown"
const content =
typeof msg.content === "string"
? msg.content
: typeof msg.text === "string"
? msg.text
: JSON.stringify(msg.content || msg.text || "")
return `[${role}]\n${content}`
})
const { writeFile } = await import("fs/promises")
const fileContent =
lines.length > 0
? lines.join("\n\n---\n\n")
: (block.summary ?? "(no content available)")
await writeFile(args.toFile as string, fileContent, "utf-8")
return `Block b${target.displayId} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${args.toFile}. Block stays compressed — context unchanged. Use read tool to access specific parts.`
}

const activeMessagesBefore = snapshotActiveMessages(messagesState)
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds)

Expand Down
Loading
Loading