Skip to content

Commit 71a37b7

Browse files
authored
Merge pull request #438 from Opencode-DCP/dev
v3.0.3 - Version bump
2 parents 2afc188 + 1792dbc commit 71a37b7

5 files changed

Lines changed: 188 additions & 3 deletions

File tree

lib/prompts/system.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ You operate in a context-constrained environment. Manage context continuously to
33
44
The ONLY tool you have for context management is \`compress\`. It replaces a contiguous portion of the conversation (inclusive) with a technical summary you produce.
55
6+
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
7+
68
OPERATING STANCE
79
Prefer short, closed, summary-safe ranges.
810
When multiple independent stale ranges exist, prefer several short compressions (in parallel when possible) over one large-range compression.

lib/tools/compress.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
type CompressToolArgs,
2323
} from "./utils"
2424
import { isIgnoredUserMessage } from "../messages/utils"
25+
import { assignMessageRefs } from "../message-ids"
2526
import { getCurrentParams, getCurrentTokenUsage, countTokens } from "../strategies/utils"
2627
import { deduplicate, supersedeWrites, purgeErrors } from "../strategies"
2728
import { saveSessionState } from "../state/persistence"
@@ -112,6 +113,8 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
112113
ctx.config.manualMode.enabled,
113114
)
114115

116+
assignMessageRefs(ctx.state, rawMessages)
117+
115118
deduplicate(ctx.state, ctx.logger, ctx.config, rawMessages)
116119
// supersedeWrites(ctx.state, ctx.logger, ctx.config, rawMessages)
117120
purgeErrors(ctx.state, ctx.logger, ctx.config, rawMessages)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "@tarquinen/opencode-dcp",
4-
"version": "3.0.2",
4+
"version": "3.0.3",
55
"type": "module",
66
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
77
"main": "./dist/index.js",

tests/compress.test.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import assert from "node:assert/strict"
2+
import test from "node:test"
3+
import { join } from "node:path"
4+
import { tmpdir } from "node:os"
5+
import { mkdirSync } from "node:fs"
6+
import { createCompressTool } from "../lib/tools/compress"
7+
import { createSessionState, type WithParts } from "../lib/state"
8+
import type { PluginConfig } from "../lib/config"
9+
import { Logger } from "../lib/logger"
10+
11+
const testDataHome = join(tmpdir(), `opencode-dcp-tests-${process.pid}`)
12+
const testConfigHome = join(tmpdir(), `opencode-dcp-config-tests-${process.pid}`)
13+
14+
process.env.XDG_DATA_HOME = testDataHome
15+
process.env.XDG_CONFIG_HOME = testConfigHome
16+
17+
mkdirSync(testDataHome, { recursive: true })
18+
mkdirSync(testConfigHome, { recursive: true })
19+
20+
function buildConfig(): PluginConfig {
21+
return {
22+
enabled: true,
23+
debug: false,
24+
pruneNotification: "off",
25+
pruneNotificationType: "chat",
26+
commands: {
27+
enabled: true,
28+
protectedTools: [],
29+
},
30+
manualMode: {
31+
enabled: false,
32+
automaticStrategies: true,
33+
},
34+
turnProtection: {
35+
enabled: false,
36+
turns: 4,
37+
},
38+
experimental: {
39+
allowSubAgents: true,
40+
customPrompts: false,
41+
},
42+
protectedFilePatterns: [],
43+
compress: {
44+
permission: "allow",
45+
showCompression: false,
46+
maxContextLimit: 100000,
47+
minContextLimit: 30000,
48+
nudgeFrequency: 5,
49+
iterationNudgeThreshold: 15,
50+
nudgeForce: "soft",
51+
flatSchema: false,
52+
protectedTools: [],
53+
protectUserMessages: false,
54+
},
55+
strategies: {
56+
deduplication: {
57+
enabled: true,
58+
protectedTools: [],
59+
},
60+
supersedeWrites: {
61+
enabled: true,
62+
},
63+
purgeErrors: {
64+
enabled: true,
65+
turns: 4,
66+
protectedTools: [],
67+
},
68+
},
69+
}
70+
}
71+
72+
function textPart(messageID: string, sessionID: string, id: string, text: string) {
73+
return {
74+
id,
75+
messageID,
76+
sessionID,
77+
type: "text" as const,
78+
text,
79+
}
80+
}
81+
82+
function buildMessages(sessionID: string): WithParts[] {
83+
return [
84+
{
85+
info: {
86+
id: "msg-subagent-prompt",
87+
role: "user",
88+
sessionID,
89+
agent: "codebase-analyzer",
90+
model: {
91+
providerID: "anthropic",
92+
modelID: "claude-test",
93+
},
94+
time: { created: 1 },
95+
} as WithParts["info"],
96+
parts: [textPart("msg-subagent-prompt", sessionID, "part-1", "Investigate the issue")],
97+
},
98+
{
99+
info: {
100+
id: "msg-assistant-1",
101+
role: "assistant",
102+
sessionID,
103+
agent: "codebase-analyzer",
104+
time: { created: 2 },
105+
} as WithParts["info"],
106+
parts: [
107+
textPart("msg-assistant-1", sessionID, "part-2", "I found the relevant code path"),
108+
],
109+
},
110+
{
111+
info: {
112+
id: "msg-user-2",
113+
role: "user",
114+
sessionID,
115+
agent: "codebase-analyzer",
116+
model: {
117+
providerID: "anthropic",
118+
modelID: "claude-test",
119+
},
120+
time: { created: 3 },
121+
} as WithParts["info"],
122+
parts: [
123+
textPart("msg-user-2", sessionID, "part-3", "Please compress the initial findings"),
124+
],
125+
},
126+
]
127+
}
128+
129+
test("compress rebuilds subagent message refs after session state was reset", async () => {
130+
const sessionID = `ses_subagent_compress_${Date.now()}`
131+
const rawMessages = buildMessages(sessionID)
132+
const state = createSessionState()
133+
state.sessionId = "ses_other"
134+
state.messageIds.byRawId.set("other-message", "m0001")
135+
state.messageIds.byRef.set("m0001", "other-message")
136+
state.messageIds.nextRef = 2
137+
138+
const logger = new Logger(false)
139+
const tool = createCompressTool({
140+
client: {
141+
session: {
142+
messages: async () => ({ data: rawMessages }),
143+
get: async () => ({ data: { parentID: "ses_parent" } }),
144+
},
145+
},
146+
state,
147+
logger,
148+
config: buildConfig(),
149+
prompts: {
150+
reload() {},
151+
getRuntimePrompts() {
152+
return { compress: "" }
153+
},
154+
},
155+
} as any)
156+
157+
const result = await tool.execute(
158+
{
159+
topic: "Subagent race fix",
160+
content: {
161+
startId: "m0001",
162+
endId: "m0002",
163+
summary: "Captured the initial investigation and follow-up request.",
164+
},
165+
},
166+
{
167+
ask: async () => {},
168+
metadata: () => {},
169+
sessionID,
170+
messageID: "msg-compress",
171+
},
172+
)
173+
174+
assert.equal(result, "Compressed 2 messages into [Compressed conversation section].")
175+
assert.equal(state.sessionId, sessionID)
176+
assert.equal(state.isSubAgent, true)
177+
assert.equal(state.messageIds.byRef.get("m0001"), "msg-assistant-1")
178+
assert.equal(state.messageIds.byRef.get("m0002"), "msg-user-2")
179+
assert.equal(state.prune.messages.blocksById.size, 1)
180+
})

0 commit comments

Comments
 (0)