Skip to content

Commit 16a2253

Browse files
author
lex
committed
feat: port upstream fixes + rebrand to @lexwdex-org/opencode-dcp
- Fix reasoning-strip.ts: exclude reasoning from stripStaleMetadata (PR Opencode-DCP#530) - Fix state persistence: persist lastCompaction + messageIds to disk (PR Opencode-DCP#547/Opencode-DCP#530) - Fix resetOnCompaction: make inert, only reset toolParameters (PR Opencode-DCP#547/Opencode-DCP#530) - Fix hooks.ts: add try/catch fail-open for chat transforms (PR Opencode-DCP#510) - Add continuation nudge after compress tool execution (PR Opencode-DCP#547) - Translate all prompts to Chinese to reduce XML hallucination (PR Opencode-DCP#547) - Fix priority label case bug in renderMessagePriorityGuidance - Rebrand package from @tarquinen/opencode-dcp to @lexwdex-org/opencode-dcp - Update all references: README, schema URLs, update.ts, tests - Update tests to match Chinese prompts and inert resetOnCompaction Published: @lexwdex-org/opencode-dcp@3.2.1 on NPM
1 parent 0657cd2 commit 16a2253

28 files changed

Lines changed: 387 additions & 243 deletions

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ body:
5858
id: dcp-version
5959
attributes:
6060
label: DCP Version
61-
description: Which version of `@tarquinen/opencode-dcp` are you using?
61+
description: Which version of `@lexwdex-org/opencode-dcp` are you using?
6262
placeholder: "e.g. 3.0.4"
6363
validations:
6464
required: true

.task_state/progress.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Progress
2+
3+
## All WPs Complete — 2026-05-26
4+
5+
### WP1: reasoning-strip.ts ✅
6+
- Removed `"reasoning"` from `stripStaleMetadata` type guard
7+
- Preserves Anthropic `bedrock.signature` on reasoning parts
8+
9+
### WP2: State persistence ✅
10+
- Added `lastCompaction` and `messageIds` to `PersistedSessionState`
11+
- `saveState()` serializes, `loadState()` restores
12+
- `ensureSessionInitialized()` copies restored fields to SessionState
13+
- `resetOnCompaction()` made inert (only resets `toolParameters`)
14+
15+
### WP3: hooks.ts fail-open ✅
16+
- `structuredClone` messages before transform chain
17+
- try/catch wraps entire transform chain
18+
- Returns original messages on error + logs
19+
20+
### WP4: Continuation nudge ✅
21+
- After compress tool completes, injects `<dcp-system-reminder>` into last non-ignored message
22+
- Early-returns from regular nudge logic
23+
- All helper functions already existed in master
24+
25+
### WP5: Chinese prompts ✅
26+
- 8 prompt files translated to Chinese
27+
- Structural changes: removed `<dcp>` wrappers, added "关键约束" section
28+
- `priorityLabelZh` mapping added
29+
- Priority label case bug fixed (`.toLowerCase()` normalization)
30+
31+
### Test fixes ✅
32+
- 4 test files updated for Chinese text + inert resetOnCompaction
33+
- 82 pass / 1 fail (pre-existing Bun nested test() limitation)
34+
35+
### Files changed: 18 (+269/-218)
36+
- Source: 14 files
37+
- Tests: 4 files

.task_state/task_plan.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# DCP Fork Fix Plan
2+
3+
## Goal
4+
Port high-value fixes from 4 upstream PRs (#550, #547, #530, #510) to master branch.
5+
6+
## Work Packages
7+
8+
### WP1: reasoning-strip.ts — Exclude reasoning from stripStaleMetadata
9+
- **Source**: PR #530 (tracycam) approach
10+
- **File**: `lib/messages/reasoning-strip.ts`
11+
- **Change**: Remove `"reasoning"` from `RELEVANT_TYPES` array (line 28)
12+
- **Why**: Anthropic reasoning parts have `signature` field that must be preserved; opencode native already handles reasoning→text conversion for model switches
13+
- **Risk**: LOW (1 file, 1 line)
14+
15+
### WP2: State persistence — Persist lastCompaction + messageIds
16+
- **Source**: PR #547 (LeXwDeX) + PR #530 (tracycam)
17+
- **Files**: `lib/state/persistence.ts`, `lib/state/state.ts`, `lib/state/utils.ts`
18+
- **Changes**:
19+
1. Add `lastCompaction` and `messageIds` to `PersistedSessionState` interface
20+
2. Save them in `saveState()`, restore in `loadState()`
21+
3. Make `resetOnCompaction()` inert — only reset `toolParameters`
22+
4. In `ensureSessionInitialized()`, restore `lastCompaction` from persisted state
23+
- **Why**: Without persistence, session restart triggers `resetOnCompaction` which wipes all DCP state
24+
- **Risk**: MEDIUM (3 files, state shape change)
25+
26+
### WP3: hooks.ts — Fail-open transforms with try/catch
27+
- **Source**: PR #510 (gmnstr)
28+
- **File**: `lib/hooks.ts`
29+
- **Change**: Wrap `createChatMessageTransformHandler` transform chain in try/catch, clone messages before transform, return original on error
30+
- **Why**: Transform failures crash the entire chat flow
31+
- **Risk**: LOW (1 file, defensive only)
32+
33+
### WP4: Continuation nudge after compress
34+
- **Source**: PR #547 (LeXwDeX)
35+
- **File**: TBD (likely `lib/hooks.ts` or compress tool handler)
36+
- **Change**: After DCP compress tool executes, inject a system-reminder nudge to continue
37+
- **Why**: Model stops generating after compress tool returns, leaving user without response
38+
- **Risk**: LOW
39+
40+
### WP5: Chinese prompts
41+
- **Source**: PR #547 (LeXwDeX) commit 568c42b
42+
- **Files**: `lib/prompts/*.ts`
43+
- **Change**: Translate all DCP prompts to Chinese
44+
- **Why**: Reduces qwen3.7-max XML tag hallucination
45+
- **Risk**: LOW (content only, no logic)
46+
47+
## Execution Order
48+
1. WP1 + WP3 (parallel, independent files)
49+
2. WP2 (depends on understanding state flow)
50+
3. WP4 (needs exploration of compress tool handler)
51+
4. WP5 (independent, can be last)
52+
53+
## Acceptance
54+
- All changes compile (`bun typecheck` passes)
55+
- Existing tests pass (`bun test`)
56+
- No regressions in DCP compress/restore flow

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Dynamic Context Pruning Plugin
22

33
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/dansmolsky)
4-
[![npm version](https://img.shields.io/npm/v/@tarquinen/opencode-dcp.svg)](https://www.npmjs.com/package/@tarquinen/opencode-dcp)
4+
[![npm version](https://img.shields.io/npm/v/@lexwdex-org/opencode-dcp.svg)](https://www.npmjs.com/package/@lexwdex-org/opencode-dcp)
55

66
Automatically reduces token usage in OpenCode by managing conversation context.
77

@@ -12,7 +12,7 @@ Automatically reduces token usage in OpenCode by managing conversation context.
1212
Install from the CLI:
1313

1414
```bash
15-
opencode plugin @tarquinen/opencode-dcp@latest --global
15+
opencode plugin @lexwdex-org/opencode-dcp@latest --global
1616
```
1717

1818
This installs the package and adds it to your global OpenCode config.
@@ -61,7 +61,7 @@ Each level overrides the previous, so project settings take priority over global
6161

6262
```jsonc
6363
{
64-
"$schema": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/master/dcp.schema.json",
64+
"$schema": "https://raw.githubusercontent.com/LeXwDeX/opencode-dynamic-context-pruning/master/dcp.schema.json",
6565
// Enable or disable the plugin
6666
"enabled": true,
6767
// Automatically update npm-installed DCP when a newer npm latest is available.

dcp.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
3-
"$id": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/master/dcp.schema.json",
3+
"$id": "https://raw.githubusercontent.com/LeXwDeX/opencode-dynamic-context-pruning/master/dcp.schema.json",
44
"title": "DCP Plugin Configuration",
55
"description": "Configuration schema for the OpenCode Dynamic Context Pruning plugin",
66
"type": "object",

lib/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ function createDefaultConfig(): void {
771771
}
772772

773773
const configContent = `{
774-
"$schema": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/master/dcp.schema.json"
774+
"$schema": "https://raw.githubusercontent.com/LeXwDeX/opencode-dynamic-context-pruning/master/dcp.schema.json"
775775
}
776776
`
777777
writeFileSync(GLOBAL_CONFIG_PATH_JSONC, configContent, "utf-8")

lib/hooks.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -122,36 +122,46 @@ export function createChatMessageTransformHandler(
122122
return
123123
}
124124

125-
stripHallucinations(output.messages)
126-
cacheSystemPromptTokens(state, output.messages)
127-
assignMessageRefs(state, output.messages)
128-
syncCompressionBlocks(state, logger, output.messages)
129-
syncToolCache(state, config, logger, output.messages)
130-
buildToolIdList(state, output.messages)
131-
prune(state, logger, config, output.messages)
132-
await injectExtendedSubAgentResults(
133-
client,
134-
state,
135-
logger,
136-
output.messages,
137-
config.experimental.allowSubAgents,
138-
)
139-
const compressionPriorities = buildPriorityMap(config, state, output.messages)
140-
prompts.reload()
141-
injectCompressNudges(
142-
state,
143-
config,
144-
logger,
145-
output.messages,
146-
prompts.getRuntimePrompts(),
147-
compressionPriorities,
148-
)
149-
injectMessageIds(state, config, output.messages, compressionPriorities)
150-
applyPendingManualTrigger(state, output.messages, logger)
151-
stripStaleMetadata(output.messages)
125+
const originalMessages = output.messages
126+
output.messages = structuredClone(output.messages)
127+
128+
try {
129+
stripHallucinations(output.messages)
130+
cacheSystemPromptTokens(state, output.messages)
131+
assignMessageRefs(state, output.messages)
132+
syncCompressionBlocks(state, logger, output.messages)
133+
syncToolCache(state, config, logger, output.messages)
134+
buildToolIdList(state, output.messages)
135+
prune(state, logger, config, output.messages)
136+
await injectExtendedSubAgentResults(
137+
client,
138+
state,
139+
logger,
140+
output.messages,
141+
config.experimental.allowSubAgents,
142+
)
143+
const compressionPriorities = buildPriorityMap(config, state, output.messages)
144+
prompts.reload()
145+
injectCompressNudges(
146+
state,
147+
config,
148+
logger,
149+
output.messages,
150+
prompts.getRuntimePrompts(),
151+
compressionPriorities,
152+
)
153+
injectMessageIds(state, config, output.messages, compressionPriorities)
154+
applyPendingManualTrigger(state, output.messages, logger)
155+
stripStaleMetadata(output.messages)
152156

153-
if (state.sessionId) {
154-
await logger.saveContext(state.sessionId, output.messages)
157+
if (state.sessionId) {
158+
await logger.saveContext(state.sessionId, output.messages)
159+
}
160+
} catch (error) {
161+
logger.error("Chat message transform failed, returning original messages", {
162+
error: error instanceof Error ? error.message : String(error),
163+
})
164+
output.messages = originalMessages
155165
}
156166
}
157167
}

lib/messages/inject/inject.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ export const injectCompressNudges = (
5050
const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant")
5151

5252
if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
53+
const continuationReminder =
54+
"\n<dcp-system-reminder>\nCompression complete. Resume your previous task.\n</dcp-system-reminder>\n"
55+
56+
if (lastMessage) {
57+
const targetMessage = lastMessage.message
58+
let injected = false
59+
for (const part of targetMessage.parts) {
60+
if (part.type === "text" && typeof part.text === "string") {
61+
part.text += continuationReminder
62+
injected = true
63+
break
64+
}
65+
}
66+
if (!injected) {
67+
targetMessage.parts.push(
68+
createSyntheticTextPart(targetMessage, continuationReminder),
69+
)
70+
}
71+
}
72+
5373
state.nudges.contextLimitAnchors.clear()
5474
state.nudges.turnNudgeAnchors.clear()
5575
state.nudges.iterationNudgeAnchors.clear()

lib/messages/reasoning-strip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function stripStaleMetadata(messages: WithParts[]): void {
2525
}
2626

2727
message.parts = message.parts.map((part) => {
28-
if (part.type !== "text" && part.type !== "tool" && part.type !== "reasoning") {
28+
if (part.type !== "text" && part.type !== "tool") {
2929
return part
3030
}
3131

lib/prompts/compress-message.ts

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
1-
export const COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
1+
export const COMPRESS_MESSAGE = `将对话中选定的单条消息折叠为详细摘要。
22
3-
THE SUMMARY
4-
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings, tool outcomes, and user intent details that matter... EVERYTHING that preserves the value of the selected message after the raw message is removed.
3+
摘要要求
4+
摘要必须详尽。捕获文件路径、函数签名、所做决策、发现的约束、关键发现、工具结果,以及重要的用户意图细节……所有在原始消息被移除后保留其价值的内容。
55
6-
USER INTENT FIDELITY
7-
When a selected message contains user intent, preserve that intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
8-
Directly quote short user instructions when that best preserves exact meaning.
6+
用户意图保真
7+
当选定消息包含用户意图时,格外小心地保留该意图。不要改变范围、约束、优先级、验收标准或请求的结果。
8+
当直接引用简短用户指令最能保留精确含义时,直接引用。
99
10-
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool output, and repetition. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
11-
If a message contains no significant technical decisions, code changes, or user requirements, produce a minimal one-line summary rather than a detailed one.
10+
同时保持精简。去除噪音:无果的失败尝试、冗长的工具输出和重复。留下的应该是纯信号——保留完整理解、零歧义的精华细节。
11+
如果消息不包含重要的技术决策、代码变更或用户需求,生成最小化的一行摘要而非详细摘要。
1212
13-
MESSAGE IDS
14-
You specify individual raw messages by ID using the injected IDs visible in the conversation:
13+
消息 ID
14+
使用对话中可见的注入 ID 按 ID 指定单条原始消息:
1515
16-
- \`mNNNN\` IDs identify raw messages
16+
- \`mNNNN\` ID 标识原始消息
1717
18-
Each message has an ID inside XML metadata tags like \`<dcp-message-id priority="high">m0007</dcp-message-id>\`.
19-
The same ID tag appears in every tool output of the message it belongs to — each unique ID identifies one complete message.
20-
Treat these tags as message metadata only, not as content to summarize. Use only the inner \`mNNNN\` value as the \`messageId\`.
21-
The \`priority\` attribute indicates relative context cost. You MUST compress high-priority messages when their full text is no longer necessary for the active task.
22-
If prior compress-tool results are present, always compress and summarize them minimally only as part of a broader compression pass. Do not invoke the compress tool solely to re-compress an earlier compression result.
23-
Messages marked as \`<dcp-message-id>BLOCKED</dcp-message-id>\` cannot be compressed.
18+
每条消息在 XML 元数据标签内有 ID,如 \`\`。
19+
相同的 ID 标签出现在该消息所属的每个工具输出中——每个唯一 ID 标识一条完整消息。
20+
将这些标签仅视为消息元数据,而非要摘要的内容。只使用内部的 \`mNNNN\` 值作为 \`messageId\`
21+
\`priority\` 属性表示相对上下文成本。当高优先级消息的完整文本对当前任务不再必要时,你必须压缩它们。
22+
如果存在之前的压缩工具结果,始终在更广泛的压缩过程中将它们最小化地压缩和摘要。不要仅为了重新压缩之前的压缩结果而调用压缩工具。
23+
标记为 \`\` 的消息不能被压缩。
2424
25-
Rules:
25+
规则:
2626
27-
- Pick each \`messageId\` directly from injected IDs visible in context.
28-
- Only use raw message IDs of the form \`mNNNN\`.
29-
- Ignore XML attributes such as \`priority\` when copying the ID; use only the inner \`mNNNN\` value.
30-
- Do not invent IDs. Use only IDs that are present in context.
27+
- 直接从上下文中可见的注入 ID 选取每个 \`messageId\`
28+
- 只使用 \`mNNNN\` 形式的原始消息 ID。
29+
- 复制 ID 时忽略 \`priority\` 等 XML 属性;只使用内部的 \`mNNNN\` 值。
30+
- 不要发明 ID。只使用上下文中存在的 ID。
3131
32-
BATCHING
33-
Select MANY messages in a single tool call when they are safe to compress.
34-
Each entry should summarize exactly one message, and the tool can receive as many entries as needed in one batch.
32+
批量处理
33+
当多条消息可以安全压缩时,在单次工具调用中选择多条消息。
34+
每个条目应恰好摘要一条消息,工具可以在一次批量中接收任意数量的条目。
3535
36-
GENERAL CLEANUP
37-
Use the topic "general cleanup" for broad cleanup passes.
38-
During general cleanup, compress all medium and high-priority messages that are not relevant to the active task.
39-
Optimize for reducing context footprint, not for grouping messages by topic.
40-
Do not compress away still-active instructions, unresolved questions, or constraints that are likely to matter soon.
41-
Prioritize the earliest messages in the context as they will be the least relevant to the active task.
42-
General cleanup should be done periodically between other normal compression tool passes, not as the primary form of compression.
36+
通用清理
37+
使用主题"通用清理"进行广泛的清理。
38+
在通用清理期间,压缩所有与当前任务无关的中高优先级消息。
39+
优化减少上下文占用,而非按主题分组消息。
40+
不要压缩掉仍然活跃的指令、未解决的问题或可能很快重要的约束。
41+
优先处理上下文中最早的消息,因为它们与当前任务的相关性最低。
42+
通用清理应定期在其他正常压缩工具调用之间进行,而非作为压缩的主要形式。
4343
`

0 commit comments

Comments
 (0)