Skip to content

Commit caf36e9

Browse files
authored
Merge pull request #443 from Opencode-DCP/dev
Dev
2 parents 71a37b7 + 2f9b1c0 commit caf36e9

8 files changed

Lines changed: 254 additions & 60 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ Each level overrides the previous, so project settings take priority over global
106106
// Soft upper threshold: above this, DCP keeps injecting strong
107107
// compression nudges (based on nudgeFrequency), so compression is
108108
// much more likely. Accepts: number or "X%" of model context window.
109-
"maxContextLimit": 100000,
109+
"maxContextLimit": 150000,
110110
// Soft lower threshold for reminder nudges: below this, turn/iteration
111111
// reminders are off (compression less likely). At/above this, reminders
112112
// are on. Accepts: number or "X%" of model context window.
113-
"minContextLimit": 30000,
113+
"minContextLimit": 50000,
114114
// Optional per-model override for maxContextLimit by providerID/modelID.
115115
// If present, this wins over the global maxContextLimit.
116116
// Accepts: number or "X%".
@@ -122,7 +122,7 @@ Each level overrides the previous, so project settings take priority over global
122122
// Optional per-model override for minContextLimit.
123123
// If present, this wins over the global minContextLimit.
124124
// "modelMinLimits": {
125-
// "openai/gpt-5.3-codex": 30000,
125+
// "openai/gpt-5.3-codex": 50000,
126126
// "anthropic/claude-sonnet-4.6": "25%"
127127
// },
128128
// How often the context-limit nudge fires (1 = every fetch, 5 = every 5th)

dcp.schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
},
142142
"maxContextLimit": {
143143
"description": "Soft upper threshold. Above this, DCP keeps sending strong compression nudges (based on nudgeFrequency), so the model is pushed to compress. Accepts number or \"X%\" of the model context window.",
144-
"default": 100000,
144+
"default": 150000,
145145
"oneOf": [
146146
{
147147
"type": "number"
@@ -154,7 +154,7 @@
154154
},
155155
"minContextLimit": {
156156
"description": "Soft lower threshold for reminder nudges. Below this, turn/iteration reminders are off (compression is less likely). At or above this, reminders are on. Accepts number or \"X%\" of the model context window.",
157-
"default": 30000,
157+
"default": 50000,
158158
"oneOf": [
159159
{
160160
"type": "number"

lib/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,8 @@ const defaultConfig: PluginConfig = {
664664
compress: {
665665
permission: "allow",
666666
showCompression: false,
667-
maxContextLimit: 100000,
668-
minContextLimit: 30000,
667+
maxContextLimit: 150000,
668+
minContextLimit: 50000,
669669
nudgeFrequency: 5,
670670
iterationNudgeThreshold: 15,
671671
nudgeForce: "soft",

lib/prompts/store.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,6 @@ function stripConditionalTag(content: string, tagName: string): string {
183183
return content.replace(regex, "")
184184
}
185185

186-
function hasTagPairMismatch(content: string, tagName: string): boolean {
187-
const openRegex = new RegExp(`<${tagName}\\b[^>]*>`, "i")
188-
const closeRegex = new RegExp(`<\/${tagName}>`, "i")
189-
return openRegex.test(content) !== closeRegex.test(content)
190-
}
191-
192186
function unwrapDcpTagIfWrapped(content: string): string {
193187
const trimmed = content.trim()
194188

@@ -205,7 +199,14 @@ function unwrapDcpTagIfWrapped(content: string): string {
205199
function normalizeReminderPromptContent(content: string): string {
206200
const normalized = content.trim()
207201

208-
if (hasTagPairMismatch(normalized, "dcp-system-reminder")) {
202+
if (!normalized) {
203+
return ""
204+
}
205+
206+
const startsWrapped = /^\s*<dcp-system-reminder\b[^>]*>/i.test(normalized)
207+
const endsWrapped = /<\/dcp-system-reminder>\s*$/i.test(normalized)
208+
209+
if (startsWrapped !== endsWrapped) {
209210
return ""
210211
}
211212

lib/tools/utils.ts

Lines changed: 17 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -409,67 +409,40 @@ export function validateSummaryPlaceholders(
409409
endReference: BoundaryReference,
410410
summaryByBlockId: Map<number, CompressionBlock>,
411411
): number[] {
412-
const issues: string[] = []
413-
414412
const boundaryOptionalIds = new Set<number>()
415413
if (startReference.kind === "compressed-block") {
416414
if (startReference.blockId === undefined) {
417-
issues.push("Failed to map boundary matches back to raw messages")
418-
} else {
419-
boundaryOptionalIds.add(startReference.blockId)
415+
throw new Error("Failed to map boundary matches back to raw messages")
420416
}
417+
boundaryOptionalIds.add(startReference.blockId)
421418
}
422419
if (endReference.kind === "compressed-block") {
423420
if (endReference.blockId === undefined) {
424-
issues.push("Failed to map boundary matches back to raw messages")
425-
} else {
426-
boundaryOptionalIds.add(endReference.blockId)
421+
throw new Error("Failed to map boundary matches back to raw messages")
427422
}
423+
boundaryOptionalIds.add(endReference.blockId)
428424
}
429425

430426
const strictRequiredIds = requiredBlockIds.filter((id) => !boundaryOptionalIds.has(id))
431427
const requiredSet = new Set(requiredBlockIds)
432-
const placeholderIds = placeholders.map((p) => p.blockId)
433-
const placeholderSet = new Set<number>()
434-
const duplicateIds = new Set<number>()
435-
436-
for (const id of placeholderIds) {
437-
if (placeholderSet.has(id)) {
438-
duplicateIds.add(id)
439-
continue
440-
}
441-
placeholderSet.add(id)
442-
}
443-
444-
const missing = strictRequiredIds.filter((id) => !placeholderSet.has(id))
445-
446-
const unknown = placeholderIds.filter((id) => !summaryByBlockId.has(id))
447-
if (unknown.length > 0) {
448-
const uniqueUnknown = [...new Set(unknown)]
449-
issues.push(
450-
`Unknown block placeholders: ${uniqueUnknown.map(formatBlockPlaceholder).join(", ")}`,
451-
)
452-
}
428+
const keptPlaceholderIds = new Set<number>()
429+
const validPlaceholders: ParsedBlockPlaceholder[] = []
453430

454-
const invalid = placeholderIds.filter((id) => !requiredSet.has(id))
455-
if (invalid.length > 0) {
456-
const uniqueInvalid = [...new Set(invalid)]
457-
issues.push(
458-
`Invalid block placeholders for selected range: ${uniqueInvalid.map(formatBlockPlaceholder).join(", ")}`,
459-
)
460-
}
431+
for (const placeholder of placeholders) {
432+
const isKnown = summaryByBlockId.has(placeholder.blockId)
433+
const isRequired = requiredSet.has(placeholder.blockId)
434+
const isDuplicate = keptPlaceholderIds.has(placeholder.blockId)
461435

462-
if (duplicateIds.size > 0) {
463-
issues.push(
464-
`Duplicate block placeholders are not allowed: ${[...duplicateIds].map(formatBlockPlaceholder).join(", ")}`,
465-
)
436+
if (isKnown && isRequired && !isDuplicate) {
437+
validPlaceholders.push(placeholder)
438+
keptPlaceholderIds.add(placeholder.blockId)
439+
}
466440
}
467441

468-
if (issues.length > 0) {
469-
throwCombinedIssues(issues)
470-
}
442+
placeholders.length = 0
443+
placeholders.push(...validPlaceholders)
471444

472-
return missing
445+
return strictRequiredIds.filter((id) => !keptPlaceholderIds.has(id))
473446
}
474447

475448
export function injectBlockPlaceholders(
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import assert from "node:assert/strict"
2+
import test from "node:test"
3+
import type { CompressionBlock } from "../lib/state"
4+
import {
5+
appendMissingBlockSummaries,
6+
injectBlockPlaceholders,
7+
parseBlockPlaceholders,
8+
validateSummaryPlaceholders,
9+
wrapCompressedSummary,
10+
type BoundaryReference,
11+
} from "../lib/tools/utils"
12+
13+
function createBlock(blockId: number, body: string): CompressionBlock {
14+
return {
15+
blockId,
16+
active: true,
17+
deactivatedByUser: false,
18+
compressedTokens: 0,
19+
topic: `Block ${blockId}`,
20+
startId: "m0001",
21+
endId: "m0002",
22+
anchorMessageId: `msg-${blockId}`,
23+
compressMessageId: `compress-${blockId}`,
24+
includedBlockIds: [],
25+
consumedBlockIds: [],
26+
parentBlockIds: [],
27+
directMessageIds: [],
28+
directToolIds: [],
29+
effectiveMessageIds: [`msg-${blockId}`],
30+
effectiveToolIds: [],
31+
createdAt: blockId,
32+
summary: wrapCompressedSummary(blockId, body),
33+
}
34+
}
35+
36+
function createMessageBoundary(messageId: string, rawIndex: number): BoundaryReference {
37+
return {
38+
kind: "message",
39+
messageId,
40+
rawIndex,
41+
}
42+
}
43+
44+
test("compress placeholder validation keeps valid placeholders and ignores invalid ones", () => {
45+
const summaryByBlockId = new Map([
46+
[1, createBlock(1, "First compressed summary")],
47+
[2, createBlock(2, "Second compressed summary")],
48+
])
49+
const summary = "Intro (b1) unknown (b9) duplicate (b1) out-of-range (b2) outro"
50+
const parsed = parseBlockPlaceholders(summary)
51+
52+
const missingBlockIds = validateSummaryPlaceholders(
53+
parsed,
54+
[1],
55+
createMessageBoundary("msg-a", 0),
56+
createMessageBoundary("msg-b", 1),
57+
summaryByBlockId,
58+
)
59+
60+
assert.deepEqual(
61+
parsed.map((placeholder) => placeholder.blockId),
62+
[1],
63+
)
64+
assert.equal(missingBlockIds.length, 0)
65+
66+
const injected = injectBlockPlaceholders(
67+
summary,
68+
parsed,
69+
summaryByBlockId,
70+
createMessageBoundary("msg-a", 0),
71+
createMessageBoundary("msg-b", 1),
72+
)
73+
74+
assert.match(injected.expandedSummary, /First compressed summary/)
75+
assert.doesNotMatch(injected.expandedSummary, /Second compressed summary/)
76+
assert.match(injected.expandedSummary, /\(b9\)/)
77+
assert.match(injected.expandedSummary, /\(b2\)/)
78+
assert.deepEqual(injected.consumedBlockIds, [1])
79+
})
80+
81+
test("compress continues by appending required block summaries the model omitted", () => {
82+
const summaryByBlockId = new Map([[1, createBlock(1, "Recovered compressed summary")]])
83+
const summary = "The model forgot to include the prior block."
84+
const parsed = parseBlockPlaceholders(summary)
85+
86+
const missingBlockIds = validateSummaryPlaceholders(
87+
parsed,
88+
[1],
89+
createMessageBoundary("msg-a", 0),
90+
createMessageBoundary("msg-b", 1),
91+
summaryByBlockId,
92+
)
93+
94+
assert.deepEqual(missingBlockIds, [1])
95+
96+
const injected = injectBlockPlaceholders(
97+
summary,
98+
parsed,
99+
summaryByBlockId,
100+
createMessageBoundary("msg-a", 0),
101+
createMessageBoundary("msg-b", 1),
102+
)
103+
const finalSummary = appendMissingBlockSummaries(
104+
injected.expandedSummary,
105+
missingBlockIds,
106+
summaryByBlockId,
107+
injected.consumedBlockIds,
108+
)
109+
110+
assert.match(
111+
finalSummary.expandedSummary,
112+
/The following previously compressed summaries were also part of this conversation section:/,
113+
)
114+
assert.match(finalSummary.expandedSummary, /### \(b1\)/)
115+
assert.match(finalSummary.expandedSummary, /Recovered compressed summary/)
116+
assert.deepEqual(finalSummary.consumedBlockIds, [1])
117+
})

tests/compress.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ function buildConfig(): PluginConfig {
4343
compress: {
4444
permission: "allow",
4545
showCompression: false,
46-
maxContextLimit: 100000,
47-
minContextLimit: 30000,
46+
maxContextLimit: 150000,
47+
minContextLimit: 50000,
4848
nudgeFrequency: 5,
4949
iterationNudgeThreshold: 15,
5050
nudgeForce: "soft",

tests/prompts.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import assert from "node:assert/strict"
2+
import test from "node:test"
3+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
4+
import { join } from "node:path"
5+
import { tmpdir } from "node:os"
6+
import { Logger } from "../lib/logger"
7+
import { PromptStore } from "../lib/prompts/store"
8+
import { SYSTEM as SYSTEM_PROMPT } from "../lib/prompts/system"
9+
10+
function createPromptStoreFixture(overrideContent?: string) {
11+
const rootDir = mkdtempSync(join(tmpdir(), "opencode-dcp-prompts-"))
12+
const configHome = join(rootDir, "config")
13+
const workspaceDir = join(rootDir, "workspace")
14+
15+
mkdirSync(configHome, { recursive: true })
16+
mkdirSync(workspaceDir, { recursive: true })
17+
18+
const previousConfigHome = process.env.XDG_CONFIG_HOME
19+
const previousOpencodeConfigDir = process.env.OPENCODE_CONFIG_DIR
20+
21+
process.env.XDG_CONFIG_HOME = configHome
22+
delete process.env.OPENCODE_CONFIG_DIR
23+
24+
if (overrideContent !== undefined) {
25+
const overrideDir = join(configHome, "opencode", "dcp-prompts", "overrides")
26+
mkdirSync(overrideDir, { recursive: true })
27+
writeFileSync(join(overrideDir, "system.md"), overrideContent, "utf-8")
28+
}
29+
30+
const store = new PromptStore(new Logger(false), workspaceDir, true)
31+
32+
return {
33+
store,
34+
cleanup() {
35+
if (previousConfigHome === undefined) {
36+
delete process.env.XDG_CONFIG_HOME
37+
} else {
38+
process.env.XDG_CONFIG_HOME = previousConfigHome
39+
}
40+
41+
if (previousOpencodeConfigDir === undefined) {
42+
delete process.env.OPENCODE_CONFIG_DIR
43+
} else {
44+
process.env.OPENCODE_CONFIG_DIR = previousOpencodeConfigDir
45+
}
46+
47+
rmSync(rootDir, { recursive: true, force: true })
48+
},
49+
}
50+
}
51+
52+
test("system prompt overrides handle reminder tags safely", async (t) => {
53+
await t.test("plain-text mentions do not invalidate copied system prompt overrides", () => {
54+
const fixture = createPromptStoreFixture(
55+
`${SYSTEM_PROMPT.trim()}\n\nExtra override line.\n`,
56+
)
57+
58+
try {
59+
const runtimeSystemPrompt = fixture.store.getRuntimePrompts().system
60+
61+
assert.match(runtimeSystemPrompt, /Extra override line\./)
62+
assert.match(runtimeSystemPrompt, /environment-injected metadata/)
63+
} finally {
64+
fixture.cleanup()
65+
}
66+
})
67+
68+
await t.test("fully wrapped overrides are normalized to a single runtime wrapper", () => {
69+
const fixture = createPromptStoreFixture(
70+
`<dcp-system-reminder>\nWrapped override body\n</dcp-system-reminder>\n`,
71+
)
72+
73+
try {
74+
const runtimeSystemPrompt = fixture.store.getRuntimePrompts().system
75+
const openingTags = runtimeSystemPrompt.match(/<dcp-system-reminder\b[^>]*>/g) ?? []
76+
const closingTags = runtimeSystemPrompt.match(/<\/dcp-system-reminder>/g) ?? []
77+
78+
assert.equal(openingTags.length, 1)
79+
assert.equal(closingTags.length, 1)
80+
assert.match(runtimeSystemPrompt, /Wrapped override body/)
81+
} finally {
82+
fixture.cleanup()
83+
}
84+
})
85+
86+
await t.test("malformed boundary wrappers are rejected", () => {
87+
const baselineFixture = createPromptStoreFixture()
88+
const malformedFixture = createPromptStoreFixture(
89+
`<dcp-system-reminder>\nMalformed override body\n`,
90+
)
91+
92+
try {
93+
const baselineSystemPrompt = baselineFixture.store.getRuntimePrompts().system
94+
const malformedSystemPrompt = malformedFixture.store.getRuntimePrompts().system
95+
96+
assert.equal(malformedSystemPrompt, baselineSystemPrompt)
97+
assert.doesNotMatch(malformedSystemPrompt, /Malformed override body/)
98+
} finally {
99+
malformedFixture.cleanup()
100+
baselineFixture.cleanup()
101+
}
102+
})
103+
})

0 commit comments

Comments
 (0)