Skip to content

Commit b75aad8

Browse files
committed
compress: add message mode
1 parent 97c03e7 commit b75aad8

18 files changed

Lines changed: 1431 additions & 280 deletions

lib/commands/manual.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ const MANUAL_MODE_OFF = "Manual mode is now OFF."
2121
const COMPRESS_TRIGGER_PROMPT = [
2222
"<compress triggered manually>",
2323
"Manual mode trigger received. You must now use the compress tool.",
24-
"Find the most significant completed section of the conversation that can be compressed into a high-fidelity technical summary.",
25-
"Choose safe boundaries and preserve all critical implementation details.",
26-
"Return after compress with a brief explanation of what range was compressed.",
24+
"Find the most significant completed conversation content that can be compressed into a high-fidelity technical summary.",
25+
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
26+
"Return after compress with a brief explanation of what content was compressed.",
2727
].join("\n\n")
2828

29-
function getTriggerPrompt(tool: "compress", state: SessionState, userFocus?: string): string {
29+
function getTriggerPrompt(
30+
tool: "compress",
31+
state: SessionState,
32+
config: PluginConfig,
33+
userFocus?: string,
34+
): string {
3035
const base = COMPRESS_TRIGGER_PROMPT
31-
const compressedBlockGuidance = buildCompressedBlockGuidance(state)
36+
const compressedBlockGuidance = buildCompressedBlockGuidance(state, config)
3237

3338
const sections = [base, compressedBlockGuidance]
3439
if (userFocus && userFocus.trim().length > 0) {
@@ -78,5 +83,5 @@ export async function handleManualTriggerCommand(
7883
tool: "compress",
7984
userFocus?: string,
8085
): Promise<string | null> {
81-
return getTriggerPrompt(tool, ctx.state, userFocus)
86+
return getTriggerPrompt(tool, ctx.state, ctx.config, userFocus)
8287
}

lib/config.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { parse } from "jsonc-parser"
55
import type { PluginInput } from "@opencode-ai/plugin"
66

77
type Permission = "ask" | "allow" | "deny"
8+
type CompressMode = "range" | "message"
89

910
export interface Deduplication {
1011
enabled: boolean
1112
protectedTools: string[]
1213
}
1314

14-
export interface CompressTool {
15+
export interface CompressConfig {
16+
mode: CompressMode
1517
permission: Permission
1618
showCompression: boolean
1719
maxContextLimit: number | `${number}%`
@@ -66,15 +68,15 @@ export interface PluginConfig {
6668
turnProtection: TurnProtection
6769
experimental: ExperimentalConfig
6870
protectedFilePatterns: string[]
69-
compress: CompressTool
71+
compress: CompressConfig
7072
strategies: {
7173
deduplication: Deduplication
7274
supersedeWrites: SupersedeWrites
7375
purgeErrors: PurgeErrors
7476
}
7577
}
7678

77-
type CompressOverride = Partial<CompressTool>
79+
type CompressOverride = Partial<CompressConfig>
7880

7981
const DEFAULT_PROTECTED_TOOLS = [
8082
"task",
@@ -112,6 +114,7 @@ export const VALID_CONFIG_KEYS = new Set([
112114
"manualMode.enabled",
113115
"manualMode.automaticStrategies",
114116
"compress",
117+
"compress.mode",
115118
"compress.permission",
116119
"compress.showCompression",
117120
"compress.maxContextLimit",
@@ -347,6 +350,18 @@ export function validateConfigTypes(config: Record<string, any>): ValidationErro
347350
actual: typeof compress,
348351
})
349352
} else {
353+
if (
354+
compress.mode !== undefined &&
355+
compress.mode !== "range" &&
356+
compress.mode !== "message"
357+
) {
358+
errors.push({
359+
key: "compress.mode",
360+
expected: '"range" | "message"',
361+
actual: JSON.stringify(compress.mode),
362+
})
363+
}
364+
350365
if (
351366
compress.nudgeFrequency !== undefined &&
352367
typeof compress.nudgeFrequency !== "number"
@@ -662,6 +677,7 @@ const defaultConfig: PluginConfig = {
662677
},
663678
protectedFilePatterns: [],
664679
compress: {
680+
mode: "range",
665681
permission: "allow",
666682
showCompression: false,
667683
maxContextLimit: 150000,
@@ -830,6 +846,7 @@ function mergeCompress(
830846
}
831847

832848
return {
849+
mode: override.mode ?? base.mode,
833850
permission: override.permission ?? base.permission,
834851
showCompression: override.showCompression ?? base.showCompression,
835852
maxContextLimit: override.maxContextLimit ?? base.maxContextLimit,

lib/messages/inject/utils.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,15 @@ export function addAnchor(
169169
return anchorMessageIds.size !== previousSize
170170
}
171171

172-
export function buildCompressedBlockGuidance(state: SessionState): string {
172+
export function buildCompressedBlockGuidance(state: SessionState, config: PluginConfig): string {
173+
if (config.compress.mode === "message") {
174+
return [
175+
"Compressed message context:",
176+
"- Message mode is active. Compress individual raw messages using `mNNNN` IDs only.",
177+
"- Do not use block placeholders or `bN` references in message mode.",
178+
].join("\n")
179+
}
180+
173181
const refs = Array.from(state.prune.messages.activeBlockIds)
174182
.filter((id) => Number.isInteger(id) && id > 0)
175183
.sort((a, b) => a - b)
@@ -238,7 +246,7 @@ export function applyAnchoredNudges(
238246
messages: WithParts[],
239247
prompts: RuntimePrompts,
240248
): void {
241-
const compressedBlockGuidance = buildCompressedBlockGuidance(state)
249+
const compressedBlockGuidance = buildCompressedBlockGuidance(state, config)
242250

243251
const contextLimitNudge = appendGuidanceToDcpTag(
244252
prompts.contextLimitNudge,

lib/prompts/compress-message.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
export const COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
2+
3+
THE PHILOSOPHY OF MESSAGE COMPRESS
4+
\`compress\` in message mode transforms specific stale messages into dense, high-fidelity summaries. This is not cleanup - it is crystallization. Your summary becomes the authoritative record of what each selected message contributed.
5+
6+
Think of compression as phase transitions: raw exploration becomes refined understanding. The original message served its purpose; your summary now carries that understanding forward.
7+
8+
THE SUMMARY
9+
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.
10+
11+
USER INTENT FIDELITY
12+
When a selected message contains user intent, preserve that intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
13+
Directly quote short user instructions when that best preserves exact meaning.
14+
15+
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.
16+
17+
MESSAGE IDS
18+
You specify individual raw messages by ID using the injected IDs visible in the conversation:
19+
20+
- \`mNNNN\` IDs identify raw messages
21+
22+
Each message has an ID inside XML metadata tags like \`<dcp-message-id>...</dcp-message-id>\`.
23+
Treat these tags as message metadata only, not as content to summarize.
24+
25+
Rules:
26+
27+
- Pick each \`messageId\` directly from injected IDs visible in context.
28+
- Only use raw message IDs of the form \`mNNNN\`.
29+
- Do NOT use compressed block IDs like \`bN\`.
30+
- Do not invent IDs. Use only IDs that are present in context.
31+
- Do not target prior compressed blocks or block summaries.
32+
33+
THE WAYS OF MESSAGE COMPRESS
34+
Compress when an individual message is genuinely closed and unlikely to be needed verbatim again:
35+
36+
Research findings have already been absorbed into later work
37+
Tool-heavy assistant updates are no longer needed in raw form
38+
Earlier planning or analysis messages are now stale but still important to retain as summary
39+
40+
Do NOT compress when:
41+
You may need the exact raw message text, code, or error output in the immediate next steps
42+
The message is still actively being referenced or edited against
43+
The target is a prior compressed block or block summary rather than a raw message
44+
45+
Before compressing, ask: _"Is this message closed enough to become summary-only right now?"_
46+
47+
BATCHING
48+
Do not call the tool once per message. Select MANY messages in a single tool call when they are independently safe to compress.
49+
Each entry should summarize exactly one message, and the tool can receive as many entries as needed in one batch.
50+
51+
THE FORMAT OF MESSAGE COMPRESS
52+
53+
~~~json
54+
{
55+
"topic": "overall batch label",
56+
"content": [
57+
{
58+
"messageId": "m0001",
59+
"topic": "short message label",
60+
"summary": "Complete technical summary replacing that one message"
61+
}
62+
]
63+
}
64+
~~~
65+
66+
Because each message is compressed independently:
67+
68+
- Do not describe ranges
69+
- Do not use start/end boundaries
70+
- Do not use compressed block placeholders
71+
- Do not reference prior compressed blocks with \`(bN)\`
72+
`
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const COMPRESS = `Collapse a range in the conversation into a detailed summary.
1+
export const COMPRESS_RANGE = `Collapse a range in the conversation into a detailed summary.
22
33
THE PHILOSOPHY OF COMPRESS
44
\`compress\` transforms verbose conversation sequences into dense, high-fidelity summaries. This is not cleanup - it is crystallization. Your summary becomes the authoritative record of what transpired.

lib/prompts/store.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@ import { join, dirname } from "path"
33
import { homedir } from "os"
44
import type { Logger } from "../logger"
55
import { SYSTEM as SYSTEM_PROMPT } from "./system"
6-
import { COMPRESS as COMPRESS_PROMPT } from "./compress"
6+
import { COMPRESS_RANGE as COMPRESS_RANGE_PROMPT } from "./compress-range"
7+
import { COMPRESS_MESSAGE as COMPRESS_MESSAGE_PROMPT } from "./compress-message"
78
import { CONTEXT_LIMIT_NUDGE } from "./context-limit-nudge"
89
import { TURN_NUDGE } from "./turn-nudge"
910
import { ITERATION_NUDGE } from "./iteration-nudge"
1011
import { MANUAL_MODE_SYSTEM_OVERLAY, SUBAGENT_SYSTEM_OVERLAY } from "./internal-overlays"
1112

1213
export type PromptKey =
1314
| "system"
14-
| "compress"
15+
| "compress-range"
16+
| "compress-message"
1517
| "context-limit-nudge"
1618
| "turn-nudge"
1719
| "iteration-nudge"
1820

1921
type EditablePromptField =
2022
| "system"
21-
| "compress"
23+
| "compressRange"
24+
| "compressMessage"
2225
| "contextLimitNudge"
2326
| "turnNudge"
2427
| "iterationNudge"
@@ -45,7 +48,8 @@ interface PromptPaths {
4548

4649
export interface RuntimePrompts {
4750
system: string
48-
compress: string
51+
compressRange: string
52+
compressMessage: string
4953
contextLimitNudge: string
5054
turnNudge: string
5155
iterationNudge: string
@@ -63,12 +67,20 @@ const PROMPT_DEFINITIONS: PromptDefinition[] = [
6367
runtimeField: "system",
6468
},
6569
{
66-
key: "compress",
67-
fileName: "compress.md",
68-
label: "Compress",
69-
description: "compress tool instructions and summary constraints",
70-
usage: "Registered as the compress tool description",
71-
runtimeField: "compress",
70+
key: "compress-range",
71+
fileName: "compress-range.md",
72+
label: "Compress Range",
73+
description: "range-mode compress tool instructions and summary constraints",
74+
usage: "Registered as the range-mode compress tool description",
75+
runtimeField: "compressRange",
76+
},
77+
{
78+
key: "compress-message",
79+
fileName: "compress-message.md",
80+
label: "Compress Message",
81+
description: "message-mode compress tool instructions and summary constraints",
82+
usage: "Registered as the message-mode compress tool description",
83+
runtimeField: "compressMessage",
7284
},
7385
{
7486
key: "context-limit-nudge",
@@ -98,7 +110,8 @@ const PROMPT_DEFINITIONS: PromptDefinition[] = [
98110

99111
export const PROMPT_KEYS: PromptKey[] = [
100112
"system",
101-
"compress",
113+
"compress-range",
114+
"compress-message",
102115
"context-limit-nudge",
103116
"turn-nudge",
104117
"iteration-nudge",
@@ -112,7 +125,8 @@ const DEFAULTS_README_FILE = "README.md"
112125

113126
const BUNDLED_EDITABLE_PROMPTS: Record<EditablePromptField, string> = {
114127
system: SYSTEM_PROMPT,
115-
compress: COMPRESS_PROMPT,
128+
compressRange: COMPRESS_RANGE_PROMPT,
129+
compressMessage: COMPRESS_MESSAGE_PROMPT,
116130
contextLimitNudge: CONTEXT_LIMIT_NUDGE,
117131
turnNudge: TURN_NUDGE,
118132
iterationNudge: ITERATION_NUDGE,
@@ -126,7 +140,8 @@ const INTERNAL_PROMPT_OVERLAYS = {
126140
function createBundledRuntimePrompts(): RuntimePrompts {
127141
return {
128142
system: BUNDLED_EDITABLE_PROMPTS.system,
129-
compress: BUNDLED_EDITABLE_PROMPTS.compress,
143+
compressRange: BUNDLED_EDITABLE_PROMPTS.compressRange,
144+
compressMessage: BUNDLED_EDITABLE_PROMPTS.compressMessage,
130145
contextLimitNudge: BUNDLED_EDITABLE_PROMPTS.contextLimitNudge,
131146
turnNudge: BUNDLED_EDITABLE_PROMPTS.turnNudge,
132147
iterationNudge: BUNDLED_EDITABLE_PROMPTS.iterationNudge,
@@ -232,7 +247,7 @@ function toEditablePromptText(definition: PromptDefinition, rawContent: string):
232247
normalized = stripConditionalTag(normalized, "subagent")
233248
}
234249

235-
if (definition.key !== "compress") {
250+
if (definition.key !== "compress-range" && definition.key !== "compress-message") {
236251
normalized = normalizeReminderPromptContent(normalized)
237252
}
238253

@@ -245,7 +260,7 @@ function wrapRuntimePromptContent(definition: PromptDefinition, editableText: st
245260
return ""
246261
}
247262

248-
if (definition.key === "compress") {
263+
if (definition.key === "compress-range" || definition.key === "compress-message") {
249264
return trimmed
250265
}
251266

lib/prompts/system.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
export const SYSTEM = `
22
You operate in a context-constrained environment. Manage context continuously to avoid buildup and preserve retrieval quality. Efficient context management is paramount for your agentic performance.
33
4-
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.
4+
The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with technical summaries you produce. Depending on the configured mode, it may compress closed ranges or selected individual messages.
55
66
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
77
88
OPERATING STANCE
9-
Prefer short, closed, summary-safe ranges.
10-
When multiple independent stale ranges exist, prefer several short compressions (in parallel when possible) over one large-range compression.
9+
Prefer short, closed, summary-safe compressions.
10+
When multiple independent stale sections exist, prefer several focused compressions (in parallel when possible) over one broad compression.
1111
1212
Use \`compress\` as steady housekeeping while you work.
1313
1414
CADENCE, SIGNALS, AND LATENCY
1515
1616
- No fixed threshold mandates compression
17-
- Prioritize closedness and independence over raw range size
17+
- Prioritize closedness and independence over raw size
1818
- Prefer smaller, regular compressions over infrequent massive compressions for better latency and summary quality
19-
- When multiple independent stale ranges are ready, batch compressions in parallel
19+
- When multiple independent stale sections are ready, batch compressions in parallel
2020
2121
DO NOT COMPRESS IF
2222
2323
- raw context is still relevant and needed for edits or precise references
24-
- the task in the target range is still actively in progress
24+
- the target content is still actively in progress
2525
26-
Evaluate conversation signal-to-noise REGULARLY. Use \`compress\` deliberately with quality-first summaries. Prefer multiple short, independent range compressions before considering broader ranges, and prioritize ranges intelligently to maintain a high-signal context window that supports your agency
26+
Evaluate conversation signal-to-noise REGULARLY. Use \`compress\` deliberately with quality-first summaries. Prefer multiple short, independent compressions before considering broader ones, and prioritize stale content intelligently to maintain a high-signal context window that supports your agency
2727
2828
It is of your responsibility to keep a sharp, high-quality context window for optimal performance
2929
`

0 commit comments

Comments
 (0)