@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.
Files changed (46) hide show
  1. package/dcp.schema.json +329 -0
  2. package/dist/lib/config.js +2 -2
  3. package/dist/lib/config.js.map +1 -1
  4. package/index.ts +141 -0
  5. package/lib/auth.ts +37 -0
  6. package/lib/commands/compression-targets.ts +137 -0
  7. package/lib/commands/context.ts +132 -0
  8. package/lib/commands/decompress.ts +275 -0
  9. package/lib/commands/help.ts +76 -0
  10. package/lib/commands/index.ts +11 -0
  11. package/lib/commands/manual.ts +125 -0
  12. package/lib/commands/recompress.ts +224 -0
  13. package/lib/commands/stats.ts +148 -0
  14. package/lib/commands/sweep.ts +268 -0
  15. package/lib/compress-permission.ts +25 -0
  16. package/lib/config.ts +2 -2
  17. package/lib/hooks.ts +378 -0
  18. package/lib/host-permissions.ts +101 -0
  19. package/lib/messages/index.ts +8 -0
  20. package/lib/messages/inject/inject.ts +215 -0
  21. package/lib/messages/inject/subagent-results.ts +82 -0
  22. package/lib/messages/inject/utils.ts +374 -0
  23. package/lib/messages/priority.ts +102 -0
  24. package/lib/messages/prune.ts +238 -0
  25. package/lib/messages/reasoning-strip.ts +40 -0
  26. package/lib/messages/sync.ts +124 -0
  27. package/lib/messages/utils.ts +187 -0
  28. package/lib/prompts/compress-message.ts +42 -0
  29. package/lib/prompts/compress-range.ts +60 -0
  30. package/lib/prompts/context-limit-nudge.ts +18 -0
  31. package/lib/prompts/extensions/nudge.ts +43 -0
  32. package/lib/prompts/extensions/system.ts +32 -0
  33. package/lib/prompts/extensions/tool.ts +35 -0
  34. package/lib/prompts/index.ts +29 -0
  35. package/lib/prompts/iteration-nudge.ts +6 -0
  36. package/lib/prompts/store.ts +467 -0
  37. package/lib/prompts/system.ts +33 -0
  38. package/lib/prompts/turn-nudge.ts +10 -0
  39. package/lib/protected-patterns.ts +128 -0
  40. package/lib/strategies/deduplication.ts +127 -0
  41. package/lib/strategies/index.ts +2 -0
  42. package/lib/strategies/purge-errors.ts +88 -0
  43. package/lib/subagents/subagent-results.ts +74 -0
  44. package/lib/ui/notification.ts +346 -0
  45. package/lib/ui/utils.ts +287 -0
  46. 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
+ }