@tarquinen/opencode-dcp 3.2.4-beta0 → 3.2.6-beta0
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/dcp.schema.json +329 -0
- package/dist/lib/config.js +2 -2
- package/dist/lib/config.js.map +1 -1
- package/index.ts +141 -0
- package/lib/auth.ts +37 -0
- package/lib/commands/compression-targets.ts +137 -0
- package/lib/commands/context.ts +132 -0
- package/lib/commands/decompress.ts +275 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +125 -0
- package/lib/commands/recompress.ts +224 -0
- package/lib/commands/stats.ts +148 -0
- package/lib/commands/sweep.ts +268 -0
- package/lib/compress-permission.ts +25 -0
- package/lib/config.ts +2 -2
- package/lib/hooks.ts +378 -0
- package/lib/host-permissions.ts +101 -0
- package/lib/messages/index.ts +8 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +82 -0
- package/lib/messages/inject/utils.ts +374 -0
- package/lib/messages/priority.ts +102 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +187 -0
- package/lib/prompts/compress-message.ts +42 -0
- package/lib/prompts/compress-range.ts +60 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +43 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +35 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +467 -0
- package/lib/prompts/system.ts +33 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +128 -0
- package/lib/strategies/deduplication.ts +127 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +88 -0
- package/lib/subagents/subagent-results.ts +74 -0
- package/lib/ui/notification.ts +346 -0
- package/lib/ui/utils.ts +287 -0
- package/package.json +14 -19
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import type { SessionState, WithParts } from "../../state"
|
|
2
|
+
import type { PluginConfig } from "../../config"
|
|
3
|
+
import {
|
|
4
|
+
appendGuidanceToDcpTag,
|
|
5
|
+
buildCompressedBlockGuidance,
|
|
6
|
+
renderMessagePriorityGuidance,
|
|
7
|
+
} from "../../prompts/extensions/nudge"
|
|
8
|
+
import type { RuntimePrompts } from "../../prompts/store"
|
|
9
|
+
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
|
10
|
+
import {
|
|
11
|
+
type CompressionPriorityMap,
|
|
12
|
+
type MessagePriority,
|
|
13
|
+
listPriorityRefsBeforeIndex,
|
|
14
|
+
} from "../priority"
|
|
15
|
+
import {
|
|
16
|
+
appendToTextPart,
|
|
17
|
+
appendToLastTextPart,
|
|
18
|
+
createSyntheticTextPart,
|
|
19
|
+
hasContent,
|
|
20
|
+
} from "../utils"
|
|
21
|
+
import { getLastUserMessage, isIgnoredUserMessage } from "../query"
|
|
22
|
+
import { getCurrentTokenUsage } from "../../token-utils"
|
|
23
|
+
import { getActiveSummaryTokenUsage } from "../../state/utils"
|
|
24
|
+
|
|
25
|
+
const MESSAGE_MODE_NUDGE_PRIORITY: MessagePriority = "high"
|
|
26
|
+
|
|
27
|
+
export interface LastUserModelContext {
|
|
28
|
+
providerId: string | undefined
|
|
29
|
+
modelId: string | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LastNonIgnoredMessage {
|
|
33
|
+
message: WithParts
|
|
34
|
+
index: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getNudgeFrequency(config: PluginConfig): number {
|
|
38
|
+
return Math.max(1, Math.floor(config.compress.nudgeFrequency || 1))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getIterationNudgeThreshold(config: PluginConfig): number {
|
|
42
|
+
return Math.max(1, Math.floor(config.compress.iterationNudgeThreshold || 1))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findLastNonIgnoredMessage(messages: WithParts[]): LastNonIgnoredMessage | null {
|
|
46
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
47
|
+
const message = messages[i]
|
|
48
|
+
if (isIgnoredUserMessage(message)) {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
return { message, index: i }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function countMessagesAfterIndex(messages: WithParts[], index: number): number {
|
|
58
|
+
let count = 0
|
|
59
|
+
|
|
60
|
+
for (let i = index + 1; i < messages.length; i++) {
|
|
61
|
+
const message = messages[i]
|
|
62
|
+
if (isIgnoredUserMessage(message)) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
count++
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return count
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getModelInfo(messages: WithParts[]): LastUserModelContext {
|
|
72
|
+
const lastUserMessage = getLastUserMessage(messages)
|
|
73
|
+
if (!lastUserMessage) {
|
|
74
|
+
return {
|
|
75
|
+
providerId: undefined,
|
|
76
|
+
modelId: undefined,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const userInfo = lastUserMessage.info as UserMessage
|
|
81
|
+
return {
|
|
82
|
+
providerId: userInfo.model.providerID,
|
|
83
|
+
modelId: userInfo.model.modelID,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveContextTokenLimit(
|
|
88
|
+
config: PluginConfig,
|
|
89
|
+
state: SessionState,
|
|
90
|
+
providerId: string | undefined,
|
|
91
|
+
modelId: string | undefined,
|
|
92
|
+
threshold: "max" | "min",
|
|
93
|
+
): number | undefined {
|
|
94
|
+
const parseLimitValue = (limit: number | `${number}%` | undefined): number | undefined => {
|
|
95
|
+
if (limit === undefined) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof limit === "number") {
|
|
100
|
+
return limit
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!limit.endsWith("%") || state.modelContextLimit === undefined) {
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parsedPercent = parseFloat(limit.slice(0, -1))
|
|
108
|
+
if (isNaN(parsedPercent)) {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const roundedPercent = Math.round(parsedPercent)
|
|
113
|
+
const clampedPercent = Math.max(0, Math.min(100, roundedPercent))
|
|
114
|
+
return Math.round((clampedPercent / 100) * state.modelContextLimit)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const modelLimits =
|
|
118
|
+
threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits
|
|
119
|
+
if (modelLimits && providerId !== undefined && modelId !== undefined) {
|
|
120
|
+
const providerModelId = `${providerId}/${modelId}`
|
|
121
|
+
const modelLimit = modelLimits[providerModelId]
|
|
122
|
+
if (modelLimit !== undefined) {
|
|
123
|
+
return parseLimitValue(modelLimit)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const globalLimit =
|
|
128
|
+
threshold === "max" ? config.compress.maxContextLimit : config.compress.minContextLimit
|
|
129
|
+
return parseLimitValue(globalLimit)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function isContextOverLimits(
|
|
133
|
+
config: PluginConfig,
|
|
134
|
+
state: SessionState,
|
|
135
|
+
providerId: string | undefined,
|
|
136
|
+
modelId: string | undefined,
|
|
137
|
+
messages: WithParts[],
|
|
138
|
+
) {
|
|
139
|
+
const summaryTokenExtension = config.compress.summaryBuffer
|
|
140
|
+
? getActiveSummaryTokenUsage(state)
|
|
141
|
+
: 0
|
|
142
|
+
const resolvedMaxContextLimit = resolveContextTokenLimit(
|
|
143
|
+
config,
|
|
144
|
+
state,
|
|
145
|
+
providerId,
|
|
146
|
+
modelId,
|
|
147
|
+
"max",
|
|
148
|
+
)
|
|
149
|
+
const maxContextLimit =
|
|
150
|
+
resolvedMaxContextLimit === undefined
|
|
151
|
+
? undefined
|
|
152
|
+
: resolvedMaxContextLimit + summaryTokenExtension
|
|
153
|
+
const minContextLimit = resolveContextTokenLimit(config, state, providerId, modelId, "min")
|
|
154
|
+
const currentTokens = getCurrentTokenUsage(state, messages)
|
|
155
|
+
|
|
156
|
+
const overMaxLimit = maxContextLimit === undefined ? false : currentTokens > maxContextLimit
|
|
157
|
+
const overMinLimit = minContextLimit === undefined ? true : currentTokens >= minContextLimit
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
overMaxLimit,
|
|
161
|
+
overMinLimit,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function addAnchor(
|
|
166
|
+
anchorMessageIds: Set<string>,
|
|
167
|
+
anchorMessageId: string,
|
|
168
|
+
anchorMessageIndex: number,
|
|
169
|
+
messages: WithParts[],
|
|
170
|
+
interval: number,
|
|
171
|
+
): boolean {
|
|
172
|
+
if (anchorMessageIndex < 0) {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let latestAnchorMessageIndex = -1
|
|
177
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
178
|
+
if (anchorMessageIds.has(messages[i].info.id)) {
|
|
179
|
+
latestAnchorMessageIndex = i
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const shouldAdd =
|
|
185
|
+
latestAnchorMessageIndex < 0 || anchorMessageIndex - latestAnchorMessageIndex >= interval
|
|
186
|
+
if (!shouldAdd) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const previousSize = anchorMessageIds.size
|
|
191
|
+
anchorMessageIds.add(anchorMessageId)
|
|
192
|
+
return anchorMessageIds.size !== previousSize
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildMessagePriorityGuidance(
|
|
196
|
+
messages: WithParts[],
|
|
197
|
+
compressionPriorities: CompressionPriorityMap | undefined,
|
|
198
|
+
anchorIndex: number,
|
|
199
|
+
priority: MessagePriority,
|
|
200
|
+
): string {
|
|
201
|
+
if (!compressionPriorities || compressionPriorities.size === 0) {
|
|
202
|
+
return ""
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const refs = listPriorityRefsBeforeIndex(messages, compressionPriorities, anchorIndex, priority)
|
|
206
|
+
const priorityLabel = `${priority[0].toUpperCase()}${priority.slice(1)}`
|
|
207
|
+
|
|
208
|
+
return renderMessagePriorityGuidance(priorityLabel, refs)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function injectAnchoredNudge(message: WithParts, nudgeText: string): void {
|
|
212
|
+
if (!nudgeText.trim()) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (message.info.role === "user") {
|
|
217
|
+
if (appendToLastTextPart(message, nudgeText)) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
message.parts.push(createSyntheticTextPart(message, nudgeText))
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (message.info.role !== "assistant") {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!hasContent(message)) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const part of message.parts) {
|
|
234
|
+
if (part.type === "text") {
|
|
235
|
+
if (appendToTextPart(part, nudgeText)) {
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const syntheticPart = createSyntheticTextPart(message, nudgeText)
|
|
242
|
+
const firstToolIndex = message.parts.findIndex((p) => p.type === "tool")
|
|
243
|
+
if (firstToolIndex === -1) {
|
|
244
|
+
message.parts.push(syntheticPart)
|
|
245
|
+
} else {
|
|
246
|
+
message.parts.splice(firstToolIndex, 0, syntheticPart)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function collectAnchoredMessages(
|
|
251
|
+
anchorMessageIds: Set<string>,
|
|
252
|
+
messages: WithParts[],
|
|
253
|
+
): Array<{ message: WithParts; index: number }> {
|
|
254
|
+
const anchoredMessages: Array<{ message: WithParts; index: number }> = []
|
|
255
|
+
|
|
256
|
+
for (const anchorMessageId of anchorMessageIds) {
|
|
257
|
+
const index = messages.findIndex((message) => message.info.id === anchorMessageId)
|
|
258
|
+
if (index === -1) {
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
anchoredMessages.push({
|
|
263
|
+
message: messages[index],
|
|
264
|
+
index,
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return anchoredMessages
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function collectTurnNudgeAnchors(
|
|
272
|
+
state: SessionState,
|
|
273
|
+
config: PluginConfig,
|
|
274
|
+
messages: WithParts[],
|
|
275
|
+
): Set<string> {
|
|
276
|
+
const turnNudgeAnchors = new Set<string>()
|
|
277
|
+
const targetRole = config.compress.nudgeForce === "strong" ? "user" : "assistant"
|
|
278
|
+
|
|
279
|
+
for (const message of messages) {
|
|
280
|
+
if (!state.nudges.turnNudgeAnchors.has(message.info.id)) continue
|
|
281
|
+
|
|
282
|
+
if (message.info.role === targetRole) {
|
|
283
|
+
turnNudgeAnchors.add(message.info.id)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return turnNudgeAnchors
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function applyRangeModeAnchoredNudge(
|
|
291
|
+
anchorMessageIds: Set<string>,
|
|
292
|
+
messages: WithParts[],
|
|
293
|
+
baseNudgeText: string,
|
|
294
|
+
compressedBlockGuidance: string,
|
|
295
|
+
): void {
|
|
296
|
+
const nudgeText = appendGuidanceToDcpTag(baseNudgeText, compressedBlockGuidance)
|
|
297
|
+
if (!nudgeText.trim()) {
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
|
|
302
|
+
injectAnchoredNudge(message, nudgeText)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function applyMessageModeAnchoredNudge(
|
|
307
|
+
anchorMessageIds: Set<string>,
|
|
308
|
+
messages: WithParts[],
|
|
309
|
+
baseNudgeText: string,
|
|
310
|
+
compressionPriorities?: CompressionPriorityMap,
|
|
311
|
+
): void {
|
|
312
|
+
for (const { message, index } of collectAnchoredMessages(anchorMessageIds, messages)) {
|
|
313
|
+
const priorityGuidance = buildMessagePriorityGuidance(
|
|
314
|
+
messages,
|
|
315
|
+
compressionPriorities,
|
|
316
|
+
index,
|
|
317
|
+
MESSAGE_MODE_NUDGE_PRIORITY,
|
|
318
|
+
)
|
|
319
|
+
const nudgeText = appendGuidanceToDcpTag(baseNudgeText, priorityGuidance)
|
|
320
|
+
injectAnchoredNudge(message, nudgeText)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function applyAnchoredNudges(
|
|
325
|
+
state: SessionState,
|
|
326
|
+
config: PluginConfig,
|
|
327
|
+
messages: WithParts[],
|
|
328
|
+
prompts: RuntimePrompts,
|
|
329
|
+
compressionPriorities?: CompressionPriorityMap,
|
|
330
|
+
): void {
|
|
331
|
+
const turnNudgeAnchors = collectTurnNudgeAnchors(state, config, messages)
|
|
332
|
+
|
|
333
|
+
if (config.compress.mode === "message") {
|
|
334
|
+
applyMessageModeAnchoredNudge(
|
|
335
|
+
state.nudges.contextLimitAnchors,
|
|
336
|
+
messages,
|
|
337
|
+
prompts.contextLimitNudge,
|
|
338
|
+
compressionPriorities,
|
|
339
|
+
)
|
|
340
|
+
applyMessageModeAnchoredNudge(
|
|
341
|
+
turnNudgeAnchors,
|
|
342
|
+
messages,
|
|
343
|
+
prompts.turnNudge,
|
|
344
|
+
compressionPriorities,
|
|
345
|
+
)
|
|
346
|
+
applyMessageModeAnchoredNudge(
|
|
347
|
+
state.nudges.iterationNudgeAnchors,
|
|
348
|
+
messages,
|
|
349
|
+
prompts.iterationNudge,
|
|
350
|
+
compressionPriorities,
|
|
351
|
+
)
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const compressedBlockGuidance = buildCompressedBlockGuidance(state)
|
|
356
|
+
applyRangeModeAnchoredNudge(
|
|
357
|
+
state.nudges.contextLimitAnchors,
|
|
358
|
+
messages,
|
|
359
|
+
prompts.contextLimitNudge,
|
|
360
|
+
compressedBlockGuidance,
|
|
361
|
+
)
|
|
362
|
+
applyRangeModeAnchoredNudge(
|
|
363
|
+
turnNudgeAnchors,
|
|
364
|
+
messages,
|
|
365
|
+
prompts.turnNudge,
|
|
366
|
+
compressedBlockGuidance,
|
|
367
|
+
)
|
|
368
|
+
applyRangeModeAnchoredNudge(
|
|
369
|
+
state.nudges.iterationNudgeAnchors,
|
|
370
|
+
messages,
|
|
371
|
+
prompts.iterationNudge,
|
|
372
|
+
compressedBlockGuidance,
|
|
373
|
+
)
|
|
374
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { PluginConfig } from "../config"
|
|
2
|
+
import { countAllMessageTokens } from "../token-utils"
|
|
3
|
+
import { isMessageCompacted } from "../state/utils"
|
|
4
|
+
import type { SessionState, WithParts } from "../state"
|
|
5
|
+
import { isIgnoredUserMessage, isProtectedUserMessage, messageHasCompress } from "./query"
|
|
6
|
+
|
|
7
|
+
const MEDIUM_PRIORITY_MIN_TOKENS = 500
|
|
8
|
+
const HIGH_PRIORITY_MIN_TOKENS = 5000
|
|
9
|
+
|
|
10
|
+
export type MessagePriority = "low" | "medium" | "high"
|
|
11
|
+
|
|
12
|
+
export interface CompressionPriorityEntry {
|
|
13
|
+
ref: string
|
|
14
|
+
tokenCount: number
|
|
15
|
+
priority: MessagePriority
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CompressionPriorityMap = Map<string, CompressionPriorityEntry>
|
|
19
|
+
|
|
20
|
+
export function buildPriorityMap(
|
|
21
|
+
config: PluginConfig,
|
|
22
|
+
state: SessionState,
|
|
23
|
+
messages: WithParts[],
|
|
24
|
+
): CompressionPriorityMap {
|
|
25
|
+
if (config.compress.mode !== "message") {
|
|
26
|
+
return new Map()
|
|
27
|
+
}
|
|
28
|
+
const priorities: CompressionPriorityMap = new Map()
|
|
29
|
+
|
|
30
|
+
for (const message of messages) {
|
|
31
|
+
if (isIgnoredUserMessage(message)) {
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isProtectedUserMessage(config, message)) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isMessageCompacted(state, message)) {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rawMessageId = message.info.id
|
|
44
|
+
if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ref = state.messageIds.byRawId.get(rawMessageId)
|
|
49
|
+
if (!ref) {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tokenCount = countAllMessageTokens(message)
|
|
54
|
+
priorities.set(rawMessageId, {
|
|
55
|
+
ref,
|
|
56
|
+
tokenCount,
|
|
57
|
+
priority: messageHasCompress(message) ? "high" : classifyMessagePriority(tokenCount),
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return priorities
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function classifyMessagePriority(tokenCount: number): MessagePriority {
|
|
65
|
+
if (tokenCount >= HIGH_PRIORITY_MIN_TOKENS) {
|
|
66
|
+
return "high"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (tokenCount >= MEDIUM_PRIORITY_MIN_TOKENS) {
|
|
70
|
+
return "medium"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return "low"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function listPriorityRefsBeforeIndex(
|
|
77
|
+
messages: WithParts[],
|
|
78
|
+
priorities: CompressionPriorityMap,
|
|
79
|
+
anchorIndex: number,
|
|
80
|
+
priority: MessagePriority,
|
|
81
|
+
): string[] {
|
|
82
|
+
const refs: string[] = []
|
|
83
|
+
const seen = new Set<string>()
|
|
84
|
+
const upperBound = Math.max(0, Math.min(anchorIndex, messages.length))
|
|
85
|
+
|
|
86
|
+
for (let index = 0; index < upperBound; index++) {
|
|
87
|
+
const rawMessageId = messages[index]?.info.id
|
|
88
|
+
if (typeof rawMessageId !== "string") {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const entry = priorities.get(rawMessageId)
|
|
93
|
+
if (!entry || entry.priority !== priority || seen.has(entry.ref)) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
seen.add(entry.ref)
|
|
98
|
+
refs.push(entry.ref)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return refs
|
|
102
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { SessionState, WithParts } from "../state"
|
|
2
|
+
import type { Logger } from "../logger"
|
|
3
|
+
import type { PluginConfig } from "../config"
|
|
4
|
+
import { isMessageCompacted } from "../state/utils"
|
|
5
|
+
import { createSyntheticUserMessage, replaceBlockIdsWithBlocked } from "./utils"
|
|
6
|
+
import { getLastUserMessage } from "./query"
|
|
7
|
+
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
|
8
|
+
|
|
9
|
+
const PRUNED_TOOL_OUTPUT_REPLACEMENT =
|
|
10
|
+
"[Output removed to save context - information superseded or no longer needed]"
|
|
11
|
+
const PRUNED_TOOL_ERROR_INPUT_REPLACEMENT = "[input removed due to failed tool call]"
|
|
12
|
+
const PRUNED_QUESTION_INPUT_REPLACEMENT = "[questions removed - see output for user's answers]"
|
|
13
|
+
|
|
14
|
+
export const prune = (
|
|
15
|
+
state: SessionState,
|
|
16
|
+
logger: Logger,
|
|
17
|
+
config: PluginConfig,
|
|
18
|
+
messages: WithParts[],
|
|
19
|
+
): void => {
|
|
20
|
+
filterCompressedRanges(state, logger, config, messages)
|
|
21
|
+
// pruneFullTool(state, logger, messages)
|
|
22
|
+
pruneToolOutputs(state, logger, messages)
|
|
23
|
+
pruneToolInputs(state, logger, messages)
|
|
24
|
+
pruneToolErrors(state, logger, messages)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pruneFullTool = (state: SessionState, logger: Logger, messages: WithParts[]): void => {
|
|
28
|
+
const messagesToRemove: string[] = []
|
|
29
|
+
|
|
30
|
+
for (const msg of messages) {
|
|
31
|
+
if (isMessageCompacted(state, msg)) {
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
36
|
+
const partsToRemove: string[] = []
|
|
37
|
+
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
if (part.type !== "tool") {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!state.prune.tools.has(part.callID)) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (part.tool !== "edit" && part.tool !== "write") {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
partsToRemove.push(part.callID)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (partsToRemove.length === 0) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
msg.parts = parts.filter(
|
|
58
|
+
(part) => part.type !== "tool" || !partsToRemove.includes(part.callID),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (msg.parts.length === 0) {
|
|
62
|
+
messagesToRemove.push(msg.info.id)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (messagesToRemove.length > 0) {
|
|
67
|
+
const result = messages.filter((msg) => !messagesToRemove.includes(msg.info.id))
|
|
68
|
+
messages.length = 0
|
|
69
|
+
messages.push(...result)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pruneToolOutputs = (state: SessionState, logger: Logger, messages: WithParts[]): void => {
|
|
74
|
+
for (const msg of messages) {
|
|
75
|
+
if (isMessageCompacted(state, msg)) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (part.type !== "tool") {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (!state.prune.tools.has(part.callID)) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (part.state.status !== "completed") {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (part.tool === "question" || part.tool === "edit" || part.tool === "write") {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const pruneToolInputs = (state: SessionState, logger: Logger, messages: WithParts[]): void => {
|
|
100
|
+
for (const msg of messages) {
|
|
101
|
+
if (isMessageCompacted(state, msg)) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
106
|
+
for (const part of parts) {
|
|
107
|
+
if (part.type !== "tool") {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!state.prune.tools.has(part.callID)) {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
if (part.state.status !== "completed") {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (part.tool !== "question") {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (part.state.input?.questions !== undefined) {
|
|
122
|
+
part.state.input.questions = PRUNED_QUESTION_INPUT_REPLACEMENT
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pruneToolErrors = (state: SessionState, logger: Logger, messages: WithParts[]): void => {
|
|
129
|
+
for (const msg of messages) {
|
|
130
|
+
if (isMessageCompacted(state, msg)) {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
135
|
+
for (const part of parts) {
|
|
136
|
+
if (part.type !== "tool") {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
if (!state.prune.tools.has(part.callID)) {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if (part.state.status !== "error") {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Prune all string inputs for errored tools
|
|
147
|
+
const input = part.state.input
|
|
148
|
+
if (input && typeof input === "object") {
|
|
149
|
+
for (const key of Object.keys(input)) {
|
|
150
|
+
if (typeof input[key] === "string") {
|
|
151
|
+
input[key] = PRUNED_TOOL_ERROR_INPUT_REPLACEMENT
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const filterCompressedRanges = (
|
|
160
|
+
state: SessionState,
|
|
161
|
+
logger: Logger,
|
|
162
|
+
config: PluginConfig,
|
|
163
|
+
messages: WithParts[],
|
|
164
|
+
): void => {
|
|
165
|
+
if (
|
|
166
|
+
state.prune.messages.byMessageId.size === 0 &&
|
|
167
|
+
state.prune.messages.activeByAnchorMessageId.size === 0
|
|
168
|
+
) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const result: WithParts[] = []
|
|
173
|
+
|
|
174
|
+
for (const msg of messages) {
|
|
175
|
+
const msgId = msg.info.id
|
|
176
|
+
|
|
177
|
+
// Check if there's a summary to inject at this anchor point
|
|
178
|
+
const blockId = state.prune.messages.activeByAnchorMessageId.get(msgId)
|
|
179
|
+
const summary =
|
|
180
|
+
blockId !== undefined ? state.prune.messages.blocksById.get(blockId) : undefined
|
|
181
|
+
if (summary) {
|
|
182
|
+
const rawSummaryContent = (summary as { summary?: unknown }).summary
|
|
183
|
+
if (
|
|
184
|
+
summary.active !== true ||
|
|
185
|
+
typeof rawSummaryContent !== "string" ||
|
|
186
|
+
rawSummaryContent.length === 0
|
|
187
|
+
) {
|
|
188
|
+
logger.warn("Skipping malformed compress summary", {
|
|
189
|
+
anchorMessageId: msgId,
|
|
190
|
+
blockId: (summary as { blockId?: unknown }).blockId,
|
|
191
|
+
})
|
|
192
|
+
} else {
|
|
193
|
+
// Find user message for variant and as base for synthetic message
|
|
194
|
+
const msgIndex = messages.indexOf(msg)
|
|
195
|
+
const userMessage = getLastUserMessage(messages, msgIndex)
|
|
196
|
+
|
|
197
|
+
if (userMessage) {
|
|
198
|
+
const userInfo = userMessage.info as UserMessage
|
|
199
|
+
const summaryContent =
|
|
200
|
+
config.compress.mode === "message"
|
|
201
|
+
? replaceBlockIdsWithBlocked(rawSummaryContent)
|
|
202
|
+
: rawSummaryContent
|
|
203
|
+
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`
|
|
204
|
+
result.push(
|
|
205
|
+
createSyntheticUserMessage(
|
|
206
|
+
userMessage,
|
|
207
|
+
summaryContent,
|
|
208
|
+
userInfo.variant,
|
|
209
|
+
summarySeed,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
logger.info("Injected compress summary", {
|
|
214
|
+
anchorMessageId: msgId,
|
|
215
|
+
summaryLength: summaryContent.length,
|
|
216
|
+
})
|
|
217
|
+
} else {
|
|
218
|
+
logger.warn("No user message found for compress summary", {
|
|
219
|
+
anchorMessageId: msgId,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Skip messages that are in the prune list
|
|
226
|
+
const pruneEntry = state.prune.messages.byMessageId.get(msgId)
|
|
227
|
+
if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Normal message, include it
|
|
232
|
+
result.push(msg)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Replace messages array contents
|
|
236
|
+
messages.length = 0
|
|
237
|
+
messages.push(...result)
|
|
238
|
+
}
|