@royalcat/opencode-dcp-rc 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/index.js +1402 -27
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/stats.d.ts +3 -1
- package/dist/lib/commands/stats.d.ts.map +1 -1
- package/dist/lib/compress/rc.d.ts.map +1 -1
- package/dist/lib/compress/summary.d.ts +11 -1
- package/dist/lib/compress/summary.d.ts.map +1 -1
- package/dist/lib/compress/types.d.ts +10 -1
- package/dist/lib/compress/types.d.ts.map +1 -1
- package/dist/lib/compress/usage.d.ts +45 -0
- package/dist/lib/compress/usage.d.ts.map +1 -0
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/system.d.ts +2 -2
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -1
- package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
- package/dist/lib/prompts/turn-nudge.d.ts +1 -1
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
- package/dist/lib/state/persistence.d.ts +2 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +3 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/tui/dialogs.d.ts.map +1 -1
- package/dist/lib/v2/index.d.ts.map +1 -1
- package/dist/lib/v2/rpc.d.ts +20 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -1
- package/dist/lib/v2/scrub.d.ts +94 -0
- package/dist/lib/v2/scrub.d.ts.map +1 -0
- package/dist/lib/v2/usage.d.ts +45 -0
- package/dist/lib/v2/usage.d.ts.map +1 -0
- package/lib/commands/stats.ts +21 -0
- package/lib/compress/rc.ts +43 -7
- package/lib/compress/summary.ts +28 -1
- package/lib/compress/types.ts +14 -1
- package/lib/compress/usage.ts +168 -0
- package/lib/config.ts +30 -0
- package/lib/prompts/context-limit-nudge.ts +2 -0
- package/lib/prompts/extensions/system.ts +4 -0
- package/lib/prompts/iteration-nudge.ts +2 -0
- package/lib/prompts/system.ts +1 -1
- package/lib/prompts/turn-nudge.ts +2 -0
- package/lib/state/persistence.ts +18 -2
- package/lib/state/state.ts +4 -0
- package/lib/state/types.ts +3 -0
- package/lib/tui/dialogs.tsx +24 -0
- package/lib/v2/index.ts +58 -5
- package/lib/v2/rpc.ts +12 -0
- package/lib/v2/scrub.ts +954 -0
- package/lib/v2/usage.ts +450 -0
- package/package.json +1 -1
package/lib/compress/rc.ts
CHANGED
|
@@ -21,6 +21,13 @@ import { countTokens } from "../token-utils"
|
|
|
21
21
|
import { saveSessionState } from "../state/persistence"
|
|
22
22
|
import { rcFormat } from "../prompts/extensions/tool"
|
|
23
23
|
import { appendProtectedPromptInfo, appendProtectedTools } from "./protected-content"
|
|
24
|
+
import {
|
|
25
|
+
addCompressionUsage,
|
|
26
|
+
addCompressionUsageTotals,
|
|
27
|
+
emptyCompressionUsage,
|
|
28
|
+
estimatedCompressionUsage,
|
|
29
|
+
type CompressionUsageTotals,
|
|
30
|
+
} from "./usage"
|
|
24
31
|
import {
|
|
25
32
|
buildSummaryPrompt,
|
|
26
33
|
parseSummarySections,
|
|
@@ -34,6 +41,16 @@ export interface RcCompressToolArgs {
|
|
|
34
41
|
ids: string[]
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
let summaryCallCounter = 0
|
|
45
|
+
|
|
46
|
+
/** Unique per hidden summary request so provider usage can be attributed. */
|
|
47
|
+
function nextSummaryCallId(): string {
|
|
48
|
+
summaryCallCounter += 1
|
|
49
|
+
return `c${summaryCallCounter.toString(36)}-${Date.now().toString(36)}-${Math.random()
|
|
50
|
+
.toString(36)
|
|
51
|
+
.slice(2, 8)}`
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
/**
|
|
38
55
|
* Compress selections with a hidden, synchronous summary request.
|
|
39
56
|
*
|
|
@@ -244,7 +261,12 @@ export function createRcCompressTool(ctx: ToolContext): ReturnType<typeof tool>
|
|
|
244
261
|
priorSummaries: plan.priorSummaries,
|
|
245
262
|
}))
|
|
246
263
|
|
|
247
|
-
const text = await generateSummaries(ctx, sessionID, segments)
|
|
264
|
+
const { text, usage } = await generateSummaries(ctx, sessionID, segments)
|
|
265
|
+
ctx.state.stats.compressionUsage = addCompressionUsageTotals(
|
|
266
|
+
ctx.state.stats.compressionUsage,
|
|
267
|
+
usage,
|
|
268
|
+
)
|
|
269
|
+
await saveSessionState(ctx.state, ctx.logger)
|
|
248
270
|
const accepted = await resolveAcceptedSections(
|
|
249
271
|
ctx,
|
|
250
272
|
plans.map((plan) => plan.selector.selector),
|
|
@@ -290,9 +312,24 @@ async function generateSummaries(
|
|
|
290
312
|
ctx: ToolContext,
|
|
291
313
|
sessionID: string,
|
|
292
314
|
segments: SummarySegment[],
|
|
293
|
-
): Promise<string> {
|
|
294
|
-
|
|
295
|
-
|
|
315
|
+
): Promise<{ text: string; usage: CompressionUsageTotals }> {
|
|
316
|
+
let usage = emptyCompressionUsage()
|
|
317
|
+
|
|
318
|
+
const summarize = async (promptSegments: SummarySegment[]): Promise<string> => {
|
|
319
|
+
const callId = nextSummaryCallId()
|
|
320
|
+
const prompt = buildSummaryPrompt(promptSegments, ctx.state.idFormat, callId)
|
|
321
|
+
const result = await ctx.generate({ sessionID, prompt, callId })
|
|
322
|
+
const text = result.text ?? ""
|
|
323
|
+
// Provider usage is authoritative; fall back to a local estimate when
|
|
324
|
+
// the plugin could not observe the underlying model call.
|
|
325
|
+
usage = addCompressionUsage(
|
|
326
|
+
usage,
|
|
327
|
+
result.usage ?? estimatedCompressionUsage(countTokens(prompt), countTokens(text)),
|
|
328
|
+
)
|
|
329
|
+
return text
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let text = await summarize(segments)
|
|
296
333
|
|
|
297
334
|
// One retry for sections the model failed to emit.
|
|
298
335
|
for (let retry = 0; retry < 2; retry++) {
|
|
@@ -311,14 +348,13 @@ async function generateSummaries(
|
|
|
311
348
|
if (retrySegments.length === 0) {
|
|
312
349
|
break
|
|
313
350
|
}
|
|
314
|
-
const retryPrompt = buildSummaryPrompt(retrySegments, ctx.state.idFormat)
|
|
315
351
|
try {
|
|
316
|
-
text += "\n\n" + (await
|
|
352
|
+
text += "\n\n" + (await summarize(retrySegments))
|
|
317
353
|
} catch {
|
|
318
354
|
break
|
|
319
355
|
}
|
|
320
356
|
}
|
|
321
|
-
return text
|
|
357
|
+
return { text, usage }
|
|
322
358
|
}
|
|
323
359
|
|
|
324
360
|
async function resolveAcceptedSections(
|
package/lib/compress/summary.ts
CHANGED
|
@@ -4,8 +4,34 @@ import { formatBlockRef, formatMessageRef, parseBoundaryId, type IdFormat } from
|
|
|
4
4
|
/**
|
|
5
5
|
* Marker embedded in the hidden summary prompt so the plugin can recognize its
|
|
6
6
|
* own transient (`session.generate`) request and strip everything else from it.
|
|
7
|
+
* Requests carry a per-call suffix (`[[DCP-RC-SUMMARY:<callId>]]`) so captured
|
|
8
|
+
* provider usage can be attributed to a specific request.
|
|
7
9
|
*/
|
|
8
10
|
export const RC_SUMMARY_MARKER = "[[DCP-RC-SUMMARY]]"
|
|
11
|
+
/** Prefix shared by the static marker and its per-call variant. */
|
|
12
|
+
export const RC_SUMMARY_MARKER_PREFIX = "[[DCP-RC-SUMMARY"
|
|
13
|
+
|
|
14
|
+
function sanitizeCallId(callId: string): string {
|
|
15
|
+
return callId.replace(/[^A-Za-z0-9_-]/g, "-")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Marker line for a hidden summary request, optionally tagged with a call id. */
|
|
19
|
+
export function buildSummaryMarker(callId?: string): string {
|
|
20
|
+
if (!callId) {
|
|
21
|
+
return RC_SUMMARY_MARKER
|
|
22
|
+
}
|
|
23
|
+
return `${RC_SUMMARY_MARKER_PREFIX}:${sanitizeCallId(callId)}]]`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when the text contains a hidden summary marker, with or without a call id. */
|
|
27
|
+
export function hasSummaryMarker(text: string): boolean {
|
|
28
|
+
return text.includes(RC_SUMMARY_MARKER_PREFIX)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extract the call id from a hidden summary marker, when present. */
|
|
32
|
+
export function extractSummaryCallId(text: string): string | undefined {
|
|
33
|
+
return text.match(/\[\[DCP-RC-SUMMARY:([A-Za-z0-9_-]+)\]\]/)?.[1]
|
|
34
|
+
}
|
|
9
35
|
|
|
10
36
|
export interface PriorSummary {
|
|
11
37
|
/** Canonical block reference, e.g. `b2` or `@b2@`. */
|
|
@@ -134,6 +160,7 @@ const SUMMARY_RULES = `RULES
|
|
|
134
160
|
export function buildSummaryPrompt(
|
|
135
161
|
selectors: SummarySegment[],
|
|
136
162
|
idFormat: IdFormat = "xml",
|
|
163
|
+
callId?: string,
|
|
137
164
|
): string {
|
|
138
165
|
const selectorList = selectors.map((segment) => segment.selector)
|
|
139
166
|
const example = selectorList[0] ?? (idFormat === "compact" ? "" : "m0004")
|
|
@@ -152,7 +179,7 @@ export function buildSummaryPrompt(
|
|
|
152
179
|
})
|
|
153
180
|
.join("\n\n")
|
|
154
181
|
|
|
155
|
-
return `${
|
|
182
|
+
return `${buildSummaryMarker(callId)}
|
|
156
183
|
|
|
157
184
|
You generate compressed-context entries for a coding session. Summarize ONLY the conversation segments listed below. Do not summarize anything else and do not ask questions.
|
|
158
185
|
|
package/lib/compress/types.ts
CHANGED
|
@@ -2,6 +2,14 @@ import type { PluginConfig } from "../config"
|
|
|
2
2
|
import type { Logger } from "../logger"
|
|
3
3
|
import type { PromptStore } from "../prompts/store"
|
|
4
4
|
import type { CompressionBlock, CompressionMode, SessionState, WithParts } from "../state"
|
|
5
|
+
import type { CompressionUsage } from "./usage"
|
|
6
|
+
|
|
7
|
+
/** Result of the hidden transient model call (OpenCode V2 `session.generate`). */
|
|
8
|
+
export interface GenerateSummaryResult {
|
|
9
|
+
text: string
|
|
10
|
+
/** Provider usage when captured, otherwise a local estimate. */
|
|
11
|
+
usage?: CompressionUsage
|
|
12
|
+
}
|
|
5
13
|
|
|
6
14
|
export interface ToolContext {
|
|
7
15
|
client: any
|
|
@@ -10,7 +18,12 @@ export interface ToolContext {
|
|
|
10
18
|
config: PluginConfig
|
|
11
19
|
prompts: PromptStore
|
|
12
20
|
/** Hidden transient model call (OpenCode V2 `session.generate`). */
|
|
13
|
-
generate: (input: {
|
|
21
|
+
generate: (input: {
|
|
22
|
+
sessionID: string
|
|
23
|
+
prompt: string
|
|
24
|
+
/** Per-request id embedded in the prompt marker for usage attribution. */
|
|
25
|
+
callId?: string
|
|
26
|
+
}) => Promise<GenerateSummaryResult>
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
export interface BoundaryReference {
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage accounting for the hidden compression (summary) request.
|
|
3
|
+
*
|
|
4
|
+
* Provider-reported usage is preferred: OpenCode V2 generates the summary
|
|
5
|
+
* through `session.generate`, which does not surface token usage to the
|
|
6
|
+
* caller, so the numbers are captured by wrapping the resolved language
|
|
7
|
+
* model (`ctx.aisdk.hook("language")`) and reading the `finish` usage.
|
|
8
|
+
* When that path is unavailable, local tokenizer estimates are recorded
|
|
9
|
+
* instead so the plugin can still report an approximate cost.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type CompressionUsageSource = "provider" | "estimated"
|
|
13
|
+
|
|
14
|
+
/** Usage of a single hidden compression request. */
|
|
15
|
+
export interface CompressionUsage {
|
|
16
|
+
inputTokens: number
|
|
17
|
+
outputTokens: number
|
|
18
|
+
cacheReadTokens: number
|
|
19
|
+
cacheWriteTokens: number
|
|
20
|
+
reasoningTokens: number
|
|
21
|
+
source: CompressionUsageSource
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Aggregated usage of all hidden compression requests in a session. */
|
|
25
|
+
export interface CompressionUsageTotals {
|
|
26
|
+
calls: number
|
|
27
|
+
providerCalls: number
|
|
28
|
+
estimatedCalls: number
|
|
29
|
+
inputTokens: number
|
|
30
|
+
outputTokens: number
|
|
31
|
+
cacheReadTokens: number
|
|
32
|
+
cacheWriteTokens: number
|
|
33
|
+
reasoningTokens: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function emptyCompressionUsage(): CompressionUsageTotals {
|
|
37
|
+
return {
|
|
38
|
+
calls: 0,
|
|
39
|
+
providerCalls: 0,
|
|
40
|
+
estimatedCalls: 0,
|
|
41
|
+
inputTokens: 0,
|
|
42
|
+
outputTokens: 0,
|
|
43
|
+
cacheReadTokens: 0,
|
|
44
|
+
cacheWriteTokens: 0,
|
|
45
|
+
reasoningTokens: 0,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toCount(value: unknown, fallback = 0): number {
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
51
|
+
return fallback
|
|
52
|
+
}
|
|
53
|
+
return Math.round(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize AI SDK v3 usage (`{ inputTokens: { total, noCache, cacheRead,
|
|
58
|
+
* cacheWrite }, outputTokens: { total, text, reasoning } }`) into the plugin
|
|
59
|
+
* shape. Returns undefined when there is nothing usable to record.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeProviderUsage(raw: unknown): CompressionUsage | undefined {
|
|
62
|
+
if (!raw || typeof raw !== "object") {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const value = raw as { inputTokens?: unknown; outputTokens?: unknown }
|
|
67
|
+
const input =
|
|
68
|
+
value.inputTokens && typeof value.inputTokens === "object"
|
|
69
|
+
? (value.inputTokens as Record<string, unknown>)
|
|
70
|
+
: {}
|
|
71
|
+
const output =
|
|
72
|
+
value.outputTokens && typeof value.outputTokens === "object"
|
|
73
|
+
? (value.outputTokens as Record<string, unknown>)
|
|
74
|
+
: {}
|
|
75
|
+
|
|
76
|
+
const cacheRead = toCount(input.cacheRead)
|
|
77
|
+
const cacheWrite = toCount(input.cacheWrite)
|
|
78
|
+
const uncached = toCount(input.noCache)
|
|
79
|
+
const inputFromParts = uncached + cacheRead + cacheWrite
|
|
80
|
+
const inputTotal = Math.max(toCount(input.total, inputFromParts), inputFromParts)
|
|
81
|
+
|
|
82
|
+
const reasoning = toCount(output.reasoning)
|
|
83
|
+
const outputTotal = Math.max(toCount(output.total, toCount(output.text) + reasoning), reasoning)
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
inputTotal === 0 &&
|
|
87
|
+
outputTotal === 0 &&
|
|
88
|
+
cacheRead === 0 &&
|
|
89
|
+
cacheWrite === 0 &&
|
|
90
|
+
reasoning === 0
|
|
91
|
+
) {
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
inputTokens: inputTotal,
|
|
97
|
+
outputTokens: outputTotal,
|
|
98
|
+
cacheReadTokens: cacheRead,
|
|
99
|
+
cacheWriteTokens: cacheWrite,
|
|
100
|
+
reasoningTokens: reasoning,
|
|
101
|
+
source: "provider",
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Local fallback when the provider did not report usage. */
|
|
106
|
+
export function estimatedCompressionUsage(
|
|
107
|
+
inputTokens: number,
|
|
108
|
+
outputTokens: number,
|
|
109
|
+
): CompressionUsage {
|
|
110
|
+
return {
|
|
111
|
+
inputTokens: toCount(inputTokens),
|
|
112
|
+
outputTokens: toCount(outputTokens),
|
|
113
|
+
cacheReadTokens: 0,
|
|
114
|
+
cacheWriteTokens: 0,
|
|
115
|
+
reasoningTokens: 0,
|
|
116
|
+
source: "estimated",
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function addCompressionUsage(
|
|
121
|
+
totals: CompressionUsageTotals,
|
|
122
|
+
usage: CompressionUsage,
|
|
123
|
+
): CompressionUsageTotals {
|
|
124
|
+
return {
|
|
125
|
+
calls: totals.calls + 1,
|
|
126
|
+
providerCalls: totals.providerCalls + (usage.source === "provider" ? 1 : 0),
|
|
127
|
+
estimatedCalls: totals.estimatedCalls + (usage.source === "estimated" ? 1 : 0),
|
|
128
|
+
inputTokens: totals.inputTokens + usage.inputTokens,
|
|
129
|
+
outputTokens: totals.outputTokens + usage.outputTokens,
|
|
130
|
+
cacheReadTokens: totals.cacheReadTokens + usage.cacheReadTokens,
|
|
131
|
+
cacheWriteTokens: totals.cacheWriteTokens + usage.cacheWriteTokens,
|
|
132
|
+
reasoningTokens: totals.reasoningTokens + usage.reasoningTokens,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function addCompressionUsageTotals(
|
|
137
|
+
totals: CompressionUsageTotals,
|
|
138
|
+
other: CompressionUsageTotals,
|
|
139
|
+
): CompressionUsageTotals {
|
|
140
|
+
return {
|
|
141
|
+
calls: totals.calls + other.calls,
|
|
142
|
+
providerCalls: totals.providerCalls + other.providerCalls,
|
|
143
|
+
estimatedCalls: totals.estimatedCalls + other.estimatedCalls,
|
|
144
|
+
inputTokens: totals.inputTokens + other.inputTokens,
|
|
145
|
+
outputTokens: totals.outputTokens + other.outputTokens,
|
|
146
|
+
cacheReadTokens: totals.cacheReadTokens + other.cacheReadTokens,
|
|
147
|
+
cacheWriteTokens: totals.cacheWriteTokens + other.cacheWriteTokens,
|
|
148
|
+
reasoningTokens: totals.reasoningTokens + other.reasoningTokens,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Backward-compatible load of persisted totals. */
|
|
153
|
+
export function normalizeCompressionUsageTotals(value: unknown): CompressionUsageTotals {
|
|
154
|
+
if (!value || typeof value !== "object") {
|
|
155
|
+
return emptyCompressionUsage()
|
|
156
|
+
}
|
|
157
|
+
const raw = value as Record<string, unknown>
|
|
158
|
+
return {
|
|
159
|
+
calls: toCount(raw.calls),
|
|
160
|
+
providerCalls: toCount(raw.providerCalls),
|
|
161
|
+
estimatedCalls: toCount(raw.estimatedCalls),
|
|
162
|
+
inputTokens: toCount(raw.inputTokens),
|
|
163
|
+
outputTokens: toCount(raw.outputTokens),
|
|
164
|
+
cacheReadTokens: toCount(raw.cacheReadTokens),
|
|
165
|
+
cacheWriteTokens: toCount(raw.cacheWriteTokens),
|
|
166
|
+
reasoningTokens: toCount(raw.reasoningTokens),
|
|
167
|
+
}
|
|
168
|
+
}
|
package/lib/config.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface CompressConfig {
|
|
|
35
35
|
protectedTools: string[]
|
|
36
36
|
protectTags: boolean
|
|
37
37
|
protectUserMessages: boolean
|
|
38
|
+
scrubModelOutput: boolean
|
|
39
|
+
scrubMessageIds: boolean
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface Commands {
|
|
@@ -131,6 +133,8 @@ export const VALID_CONFIG_KEYS = new Set([
|
|
|
131
133
|
"compress.protectedTools",
|
|
132
134
|
"compress.protectTags",
|
|
133
135
|
"compress.protectUserMessages",
|
|
136
|
+
"compress.scrubModelOutput",
|
|
137
|
+
"compress.scrubMessageIds",
|
|
134
138
|
"strategies",
|
|
135
139
|
"strategies.deduplication",
|
|
136
140
|
"strategies.deduplication.enabled",
|
|
@@ -432,6 +436,28 @@ export function validateConfigTypes(config: Record<string, any>): ValidationErro
|
|
|
432
436
|
})
|
|
433
437
|
}
|
|
434
438
|
|
|
439
|
+
if (
|
|
440
|
+
compress.scrubModelOutput !== undefined &&
|
|
441
|
+
typeof compress.scrubModelOutput !== "boolean"
|
|
442
|
+
) {
|
|
443
|
+
errors.push({
|
|
444
|
+
key: "compress.scrubModelOutput",
|
|
445
|
+
expected: "boolean",
|
|
446
|
+
actual: typeof compress.scrubModelOutput,
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
compress.scrubMessageIds !== undefined &&
|
|
452
|
+
typeof compress.scrubMessageIds !== "boolean"
|
|
453
|
+
) {
|
|
454
|
+
errors.push({
|
|
455
|
+
key: "compress.scrubMessageIds",
|
|
456
|
+
expected: "boolean",
|
|
457
|
+
actual: typeof compress.scrubMessageIds,
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
435
461
|
if (
|
|
436
462
|
typeof compress.iterationNudgeThreshold === "number" &&
|
|
437
463
|
compress.iterationNudgeThreshold < 1
|
|
@@ -676,6 +702,8 @@ const defaultConfig: PluginConfig = {
|
|
|
676
702
|
protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
|
|
677
703
|
protectTags: false,
|
|
678
704
|
protectUserMessages: false,
|
|
705
|
+
scrubModelOutput: true,
|
|
706
|
+
scrubMessageIds: true,
|
|
679
707
|
},
|
|
680
708
|
strategies: {
|
|
681
709
|
deduplication: {
|
|
@@ -844,6 +872,8 @@ function mergeCompress(
|
|
|
844
872
|
protectedTools: [...new Set([...base.protectedTools, ...(override.protectedTools ?? [])])],
|
|
845
873
|
protectTags: override.protectTags ?? base.protectTags,
|
|
846
874
|
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
875
|
+
scrubModelOutput: override.scrubModelOutput ?? base.scrubModelOutput,
|
|
876
|
+
scrubMessageIds: override.scrubMessageIds ?? base.scrubMessageIds,
|
|
847
877
|
}
|
|
848
878
|
}
|
|
849
879
|
|
|
@@ -14,5 +14,7 @@ Avoid the newest active working messages unless it is clearly closed.
|
|
|
14
14
|
SUMMARY REQUIREMENTS
|
|
15
15
|
Your summary MUST cover all essential details from the selected messages so work can continue.
|
|
16
16
|
If the compressed range includes user messages, preserve user intent exactly. Prefer direct quotes for short user messages to avoid semantic drift.
|
|
17
|
+
|
|
18
|
+
Never repeat or quote this reminder in your reply.
|
|
17
19
|
</dcp-system-reminder>
|
|
18
20
|
`
|
|
@@ -6,6 +6,8 @@ Only use the compress tool after seeing \`<compress triggered manually>\` in the
|
|
|
6
6
|
Issue exactly ONE compress tool per manual trigger. Do NOT launch multiple compress tools in parallel. Each trigger grants a single compression; after it completes, wait for the next trigger.
|
|
7
7
|
|
|
8
8
|
After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.
|
|
9
|
+
|
|
10
|
+
Never repeat this notice in your reply.
|
|
9
11
|
</dcp-system-reminder>
|
|
10
12
|
`
|
|
11
13
|
|
|
@@ -15,6 +17,8 @@ You are operating in a subagent environment.
|
|
|
15
17
|
The initial subagent instruction is imperative and must be followed exactly.
|
|
16
18
|
It is the only user message intentionally not assigned a message ID, and therefore is not eligible for compression.
|
|
17
19
|
All subsequent messages in the session will have IDs.
|
|
20
|
+
|
|
21
|
+
Never repeat this notice in your reply.
|
|
18
22
|
</dcp-system-reminder>
|
|
19
23
|
`
|
|
20
24
|
|
|
@@ -2,5 +2,7 @@ export const ITERATION_NUDGE = `<dcp-system-reminder>
|
|
|
2
2
|
You've been iterating for a while after the last user message.
|
|
3
3
|
|
|
4
4
|
If there is a closed portion that is unlikely to be referenced immediately (for example, finished research before implementation), use the compress tool on it now.
|
|
5
|
+
|
|
6
|
+
Never repeat or quote this reminder in your reply.
|
|
5
7
|
</dcp-system-reminder>
|
|
6
8
|
`
|
package/lib/prompts/system.ts
CHANGED
|
@@ -8,7 +8,7 @@ You operate in a context-constrained environment. Manage context continuously to
|
|
|
8
8
|
|
|
9
9
|
The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with compact technical summaries.
|
|
10
10
|
|
|
11
|
-
${markers} and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
11
|
+
${markers} and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output, quote, or mention them in your replies or reasoning.
|
|
12
12
|
|
|
13
13
|
THE PHILOSOPHY OF COMPRESS
|
|
14
14
|
\`compress\` transforms conversation content into dense, high-fidelity summaries. This is not cleanup - it is crystallization. Your summary becomes the authoritative record of what transpired.
|
|
@@ -6,5 +6,7 @@ If direction has shifted, compress earlier ranges that are now less relevant.
|
|
|
6
6
|
|
|
7
7
|
The goal is to filter noise and distill key information so context accumulation stays under control.
|
|
8
8
|
Keep active context uncompressed.
|
|
9
|
+
|
|
10
|
+
Never repeat or quote this reminder in your reply.
|
|
9
11
|
</dcp-system-reminder>
|
|
10
12
|
`
|
package/lib/state/persistence.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { homedir } from "os"
|
|
|
10
10
|
import { join } from "path"
|
|
11
11
|
import type { CompressionBlock, PrunedMessageEntry, SessionState, SessionStats } from "./types"
|
|
12
12
|
import type { Logger } from "../logger"
|
|
13
|
+
import {
|
|
14
|
+
addCompressionUsageTotals,
|
|
15
|
+
emptyCompressionUsage,
|
|
16
|
+
normalizeCompressionUsageTotals,
|
|
17
|
+
type CompressionUsageTotals,
|
|
18
|
+
} from "../compress/usage"
|
|
13
19
|
import { serializePruneMessagesState } from "./utils"
|
|
14
20
|
|
|
15
21
|
/** Prune state as stored on disk */
|
|
@@ -227,6 +233,7 @@ function emptyPersistedState(manualMode: boolean): PersistedSessionState {
|
|
|
227
233
|
stats: {
|
|
228
234
|
pruneTokenCounter: 0,
|
|
229
235
|
totalPruneTokens: 0,
|
|
236
|
+
compressionUsage: emptyCompressionUsage(),
|
|
230
237
|
},
|
|
231
238
|
lastUpdated: new Date().toISOString(),
|
|
232
239
|
}
|
|
@@ -257,6 +264,7 @@ export interface AggregatedStats {
|
|
|
257
264
|
totalTools: number
|
|
258
265
|
totalMessages: number
|
|
259
266
|
sessionCount: number
|
|
267
|
+
compressionUsage: CompressionUsageTotals
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
export async function loadAllSessionStats(logger: Logger): Promise<AggregatedStats> {
|
|
@@ -265,6 +273,7 @@ export async function loadAllSessionStats(logger: Logger): Promise<AggregatedSta
|
|
|
265
273
|
totalTools: 0,
|
|
266
274
|
totalMessages: 0,
|
|
267
275
|
sessionCount: 0,
|
|
276
|
+
compressionUsage: emptyCompressionUsage(),
|
|
268
277
|
}
|
|
269
278
|
|
|
270
279
|
try {
|
|
@@ -281,14 +290,21 @@ export async function loadAllSessionStats(logger: Logger): Promise<AggregatedSta
|
|
|
281
290
|
const content = await fs.readFile(filePath, "utf-8")
|
|
282
291
|
const state = JSON.parse(content) as PersistedSessionState
|
|
283
292
|
|
|
284
|
-
|
|
285
|
-
|
|
293
|
+
const usage = normalizeCompressionUsageTotals(state?.stats?.compressionUsage)
|
|
294
|
+
const hasUsage = usage.calls > 0
|
|
295
|
+
|
|
296
|
+
if (state?.prune && (state?.stats?.totalPruneTokens || hasUsage)) {
|
|
297
|
+
result.totalTokens += state.stats?.totalPruneTokens || 0
|
|
286
298
|
result.totalTools += state.prune.tools
|
|
287
299
|
? Object.keys(state.prune.tools).length
|
|
288
300
|
: 0
|
|
289
301
|
result.totalMessages += state.prune.messages?.byMessageId
|
|
290
302
|
? Object.keys(state.prune.messages.byMessageId).length
|
|
291
303
|
: 0
|
|
304
|
+
result.compressionUsage = addCompressionUsageTotals(
|
|
305
|
+
result.compressionUsage,
|
|
306
|
+
usage,
|
|
307
|
+
)
|
|
292
308
|
result.sessionCount++
|
|
293
309
|
}
|
|
294
310
|
} catch {
|
package/lib/state/state.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionState, ToolParameterEntry, WithParts } from "./types"
|
|
2
2
|
import type { Logger } from "../logger"
|
|
3
|
+
import { emptyCompressionUsage, normalizeCompressionUsageTotals } from "../compress/usage"
|
|
3
4
|
import { applyPendingCompressionDurations } from "../compress/timing"
|
|
4
5
|
import { loadManualModeSetting, loadSessionState, saveSessionState } from "./persistence"
|
|
5
6
|
import {
|
|
@@ -84,6 +85,7 @@ export function createSessionState(idFormat: IdFormat = "xml"): SessionState {
|
|
|
84
85
|
stats: {
|
|
85
86
|
pruneTokenCounter: 0,
|
|
86
87
|
totalPruneTokens: 0,
|
|
88
|
+
compressionUsage: emptyCompressionUsage(),
|
|
87
89
|
},
|
|
88
90
|
compressionTiming: {
|
|
89
91
|
startsByCallId: new Map<string, number>(),
|
|
@@ -122,6 +124,7 @@ export function resetSessionState(state: SessionState): void {
|
|
|
122
124
|
state.stats = {
|
|
123
125
|
pruneTokenCounter: 0,
|
|
124
126
|
totalPruneTokens: 0,
|
|
127
|
+
compressionUsage: emptyCompressionUsage(),
|
|
125
128
|
}
|
|
126
129
|
state.toolParameters.clear()
|
|
127
130
|
state.subAgentResultCache.clear()
|
|
@@ -186,6 +189,7 @@ export async function ensureSessionInitialized(
|
|
|
186
189
|
state.stats = {
|
|
187
190
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
188
191
|
totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
|
|
192
|
+
compressionUsage: normalizeCompressionUsageTotals(persisted.stats?.compressionUsage),
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
const applied = applyPendingCompressionDurations(state)
|
package/lib/state/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CompressionTimingState } from "../compress/timing"
|
|
2
|
+
import type { CompressionUsageTotals } from "../compress/usage"
|
|
2
3
|
import type { IdFormat } from "../message-ids"
|
|
3
4
|
import { Message, Part } from "@opencode-ai/sdk/v2"
|
|
4
5
|
|
|
@@ -22,6 +23,8 @@ export interface ToolParameterEntry {
|
|
|
22
23
|
export interface SessionStats {
|
|
23
24
|
pruneTokenCounter: number
|
|
24
25
|
totalPruneTokens: number
|
|
26
|
+
/** Token usage of the hidden compression (summary) requests. */
|
|
27
|
+
compressionUsage: CompressionUsageTotals
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface PrunedMessageEntry {
|
package/lib/tui/dialogs.tsx
CHANGED
|
@@ -121,6 +121,24 @@ export function StatsDialog(props: { api: ViewApi; report: StatsReport; onBack:
|
|
|
121
121
|
label="Messages pruned"
|
|
122
122
|
value={`${props.report.sessionMessages}`}
|
|
123
123
|
/>
|
|
124
|
+
<Metric
|
|
125
|
+
theme={theme}
|
|
126
|
+
label="Summary requests"
|
|
127
|
+
value={`${props.report.sessionCompressionUsage.calls}`}
|
|
128
|
+
hint={`${props.report.sessionCompressionUsage.providerCalls} provider · ${props.report.sessionCompressionUsage.estimatedCalls} estimated`}
|
|
129
|
+
/>
|
|
130
|
+
<Metric
|
|
131
|
+
theme={theme}
|
|
132
|
+
label="Request in|out"
|
|
133
|
+
value={`~${formatTokenCount(props.report.sessionCompressionUsage.inputTokens)} | ~${formatTokenCount(props.report.sessionCompressionUsage.outputTokens)}`}
|
|
134
|
+
hint="tokens"
|
|
135
|
+
/>
|
|
136
|
+
<Metric
|
|
137
|
+
theme={theme}
|
|
138
|
+
label="Request cache r|w"
|
|
139
|
+
value={`~${formatTokenCount(props.report.sessionCompressionUsage.cacheReadTokens)} | ~${formatTokenCount(props.report.sessionCompressionUsage.cacheWriteTokens)}`}
|
|
140
|
+
hint={`reasoning ~${formatTokenCount(props.report.sessionCompressionUsage.reasoningTokens)}`}
|
|
141
|
+
/>
|
|
124
142
|
</Card>
|
|
125
143
|
<Card theme={theme} title="All time">
|
|
126
144
|
<Metric
|
|
@@ -144,6 +162,12 @@ export function StatsDialog(props: { api: ViewApi; report: StatsReport; onBack:
|
|
|
144
162
|
label="Sessions with DCP history"
|
|
145
163
|
value={`${props.report.allTime.sessionCount}`}
|
|
146
164
|
/>
|
|
165
|
+
<Metric
|
|
166
|
+
theme={theme}
|
|
167
|
+
label="Summary requests"
|
|
168
|
+
value={`${props.report.allTime.compressionUsage.calls}`}
|
|
169
|
+
hint={`~${formatTokenCount(props.report.allTime.compressionUsage.inputTokens)} in · ~${formatTokenCount(props.report.allTime.compressionUsage.outputTokens)} out`}
|
|
170
|
+
/>
|
|
147
171
|
</Card>
|
|
148
172
|
</DcpFrame>
|
|
149
173
|
)
|