@tarquinen/opencode-dcp 3.2.5-beta0 → 3.2.7-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 (74) hide show
  1. package/dist/lib/token-utils.js +2 -2
  2. package/dist/lib/token-utils.js.map +1 -1
  3. package/index.ts +141 -0
  4. package/lib/analysis/tokens.ts +225 -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/index.ts +3 -0
  16. package/lib/compress/message-utils.ts +250 -0
  17. package/lib/compress/message.ts +137 -0
  18. package/lib/compress/pipeline.ts +106 -0
  19. package/lib/compress/protected-content.ts +154 -0
  20. package/lib/compress/range-utils.ts +308 -0
  21. package/lib/compress/range.ts +180 -0
  22. package/lib/compress/search.ts +267 -0
  23. package/lib/compress/state.ts +268 -0
  24. package/lib/compress/timing.ts +77 -0
  25. package/lib/compress/types.ts +108 -0
  26. package/lib/compress-permission.ts +25 -0
  27. package/lib/config.ts +1071 -0
  28. package/lib/hooks.ts +378 -0
  29. package/lib/host-permissions.ts +101 -0
  30. package/lib/logger.ts +235 -0
  31. package/lib/message-ids.ts +172 -0
  32. package/lib/messages/index.ts +8 -0
  33. package/lib/messages/inject/inject.ts +215 -0
  34. package/lib/messages/inject/subagent-results.ts +82 -0
  35. package/lib/messages/inject/utils.ts +374 -0
  36. package/lib/messages/priority.ts +102 -0
  37. package/lib/messages/prune.ts +238 -0
  38. package/lib/messages/query.ts +56 -0
  39. package/lib/messages/reasoning-strip.ts +40 -0
  40. package/lib/messages/sync.ts +124 -0
  41. package/lib/messages/utils.ts +187 -0
  42. package/lib/prompts/compress-message.ts +42 -0
  43. package/lib/prompts/compress-range.ts +60 -0
  44. package/lib/prompts/context-limit-nudge.ts +18 -0
  45. package/lib/prompts/extensions/nudge.ts +43 -0
  46. package/lib/prompts/extensions/system.ts +32 -0
  47. package/lib/prompts/extensions/tool.ts +35 -0
  48. package/lib/prompts/index.ts +29 -0
  49. package/lib/prompts/iteration-nudge.ts +6 -0
  50. package/lib/prompts/store.ts +467 -0
  51. package/lib/prompts/system.ts +33 -0
  52. package/lib/prompts/turn-nudge.ts +10 -0
  53. package/lib/protected-patterns.ts +128 -0
  54. package/lib/state/index.ts +4 -0
  55. package/lib/state/persistence.ts +256 -0
  56. package/lib/state/state.ts +190 -0
  57. package/lib/state/tool-cache.ts +98 -0
  58. package/lib/state/types.ts +112 -0
  59. package/lib/state/utils.ts +334 -0
  60. package/lib/strategies/deduplication.ts +127 -0
  61. package/lib/strategies/index.ts +2 -0
  62. package/lib/strategies/purge-errors.ts +88 -0
  63. package/lib/subagents/subagent-results.ts +74 -0
  64. package/lib/token-utils.ts +162 -0
  65. package/lib/ui/notification.ts +346 -0
  66. package/lib/ui/utils.ts +287 -0
  67. package/package.json +12 -3
  68. package/tui/data/context.ts +177 -0
  69. package/tui/index.tsx +34 -0
  70. package/tui/routes/summary.tsx +175 -0
  71. package/tui/shared/names.ts +9 -0
  72. package/tui/shared/theme.ts +58 -0
  73. package/tui/shared/types.ts +38 -0
  74. package/tui/slots/sidebar-content.tsx +502 -0
@@ -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
+ }
@@ -0,0 +1,56 @@
1
+ import type { PluginConfig } from "../config"
2
+ import type { WithParts } from "../state"
3
+
4
+ export const getLastUserMessage = (
5
+ messages: WithParts[],
6
+ startIndex?: number,
7
+ ): WithParts | null => {
8
+ const start = startIndex ?? messages.length - 1
9
+ for (let i = start; i >= 0; i--) {
10
+ const msg = messages[i]
11
+ if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
12
+ return msg
13
+ }
14
+ }
15
+ return null
16
+ }
17
+
18
+ export const messageHasCompress = (message: WithParts): boolean => {
19
+ if (message.info.role !== "assistant") {
20
+ return false
21
+ }
22
+
23
+ const parts = Array.isArray(message.parts) ? message.parts : []
24
+ return parts.some(
25
+ (part) =>
26
+ part.type === "tool" && part.tool === "compress" && part.state?.status === "completed",
27
+ )
28
+ }
29
+
30
+ export const isIgnoredUserMessage = (message: WithParts): boolean => {
31
+ if (message.info.role !== "user") {
32
+ return false
33
+ }
34
+
35
+ const parts = Array.isArray(message.parts) ? message.parts : []
36
+ if (parts.length === 0) {
37
+ return true
38
+ }
39
+
40
+ for (const part of parts) {
41
+ if (!(part as any).ignored) {
42
+ return false
43
+ }
44
+ }
45
+
46
+ return true
47
+ }
48
+
49
+ export function isProtectedUserMessage(config: PluginConfig, message: WithParts): boolean {
50
+ return (
51
+ config.compress.mode === "message" &&
52
+ config.compress.protectUserMessages &&
53
+ message.info.role === "user" &&
54
+ !isIgnoredUserMessage(message)
55
+ )
56
+ }
@@ -0,0 +1,40 @@
1
+ import type { WithParts } from "../state"
2
+ import { getLastUserMessage } from "./query"
3
+
4
+ /**
5
+ * Mirrors opencode's differentModel handling by preserving part content while
6
+ * dropping provider metadata on assistant parts that came from a different
7
+ * model/provider than the current turn's user message.
8
+ */
9
+ export function stripStaleMetadata(messages: WithParts[]): void {
10
+ const lastUserMessage = getLastUserMessage(messages)
11
+ if (lastUserMessage?.info.role !== "user") {
12
+ return
13
+ }
14
+
15
+ const modelID = lastUserMessage.info.model.modelID
16
+ const providerID = lastUserMessage.info.model.providerID
17
+
18
+ messages.forEach((message) => {
19
+ if (message.info.role !== "assistant") {
20
+ return
21
+ }
22
+
23
+ if (message.info.modelID === modelID && message.info.providerID === providerID) {
24
+ return
25
+ }
26
+
27
+ message.parts = message.parts.map((part) => {
28
+ if (part.type !== "text" && part.type !== "tool" && part.type !== "reasoning") {
29
+ return part
30
+ }
31
+
32
+ if (!("metadata" in part)) {
33
+ return part
34
+ }
35
+
36
+ const { metadata: _metadata, ...rest } = part
37
+ return rest
38
+ })
39
+ })
40
+ }
@@ -0,0 +1,124 @@
1
+ import type { SessionState, WithParts } from "../state"
2
+ import type { Logger } from "../logger"
3
+
4
+ function sortBlocksByCreation(
5
+ a: { createdAt: number; blockId: number },
6
+ b: { createdAt: number; blockId: number },
7
+ ): number {
8
+ const createdAtDiff = a.createdAt - b.createdAt
9
+ if (createdAtDiff !== 0) {
10
+ return createdAtDiff
11
+ }
12
+ return a.blockId - b.blockId
13
+ }
14
+
15
+ export const syncCompressionBlocks = (
16
+ state: SessionState,
17
+ logger: Logger,
18
+ messages: WithParts[],
19
+ ): void => {
20
+ const messagesState = state.prune.messages
21
+ if (!messagesState?.blocksById?.size) {
22
+ return
23
+ }
24
+
25
+ const messageIds = new Set(messages.map((msg) => msg.info.id))
26
+ const previousActiveBlockIds = new Set<number>(
27
+ Array.from(messagesState.blocksById.values())
28
+ .filter((block) => block.active)
29
+ .map((block) => block.blockId),
30
+ )
31
+
32
+ messagesState.activeBlockIds.clear()
33
+ messagesState.activeByAnchorMessageId.clear()
34
+
35
+ const now = Date.now()
36
+ const missingOriginBlockIds: number[] = []
37
+ const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation)
38
+
39
+ for (const block of orderedBlocks) {
40
+ const hasOriginMessage =
41
+ typeof block.compressMessageId === "string" &&
42
+ block.compressMessageId.length > 0 &&
43
+ messageIds.has(block.compressMessageId)
44
+
45
+ if (!hasOriginMessage) {
46
+ block.active = false
47
+ block.deactivatedAt = now
48
+ block.deactivatedByBlockId = undefined
49
+ missingOriginBlockIds.push(block.blockId)
50
+ continue
51
+ }
52
+
53
+ if (block.deactivatedByUser) {
54
+ block.active = false
55
+ if (block.deactivatedAt === undefined) {
56
+ block.deactivatedAt = now
57
+ }
58
+ block.deactivatedByBlockId = undefined
59
+ continue
60
+ }
61
+
62
+ for (const consumedBlockId of block.consumedBlockIds) {
63
+ if (!messagesState.activeBlockIds.has(consumedBlockId)) {
64
+ continue
65
+ }
66
+
67
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId)
68
+ if (consumedBlock) {
69
+ consumedBlock.active = false
70
+ consumedBlock.deactivatedAt = now
71
+ consumedBlock.deactivatedByBlockId = block.blockId
72
+
73
+ const mappedBlockId = messagesState.activeByAnchorMessageId.get(
74
+ consumedBlock.anchorMessageId,
75
+ )
76
+ if (mappedBlockId === consumedBlock.blockId) {
77
+ messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId)
78
+ }
79
+ }
80
+
81
+ messagesState.activeBlockIds.delete(consumedBlockId)
82
+ }
83
+
84
+ block.active = true
85
+ block.deactivatedAt = undefined
86
+ block.deactivatedByBlockId = undefined
87
+ messagesState.activeBlockIds.add(block.blockId)
88
+ if (messageIds.has(block.anchorMessageId)) {
89
+ messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId)
90
+ }
91
+ }
92
+
93
+ for (const entry of messagesState.byMessageId.values()) {
94
+ const allBlockIds = Array.isArray(entry.allBlockIds)
95
+ ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))]
96
+ : []
97
+
98
+ entry.allBlockIds = allBlockIds
99
+ entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id))
100
+ }
101
+
102
+ const nextActiveBlockIds = messagesState.activeBlockIds
103
+ let deactivatedCount = 0
104
+ let reactivatedCount = 0
105
+
106
+ for (const blockId of previousActiveBlockIds) {
107
+ if (!nextActiveBlockIds.has(blockId)) {
108
+ deactivatedCount++
109
+ }
110
+ }
111
+ for (const blockId of nextActiveBlockIds) {
112
+ if (!previousActiveBlockIds.has(blockId)) {
113
+ reactivatedCount++
114
+ }
115
+ }
116
+
117
+ if (missingOriginBlockIds.length > 0 || deactivatedCount > 0 || reactivatedCount > 0) {
118
+ logger.info("Synced compress block state", {
119
+ missingOriginCount: missingOriginBlockIds.length,
120
+ deactivatedCount,
121
+ reactivatedCount,
122
+ })
123
+ }
124
+ }
@@ -0,0 +1,187 @@
1
+ import { createHash } from "node:crypto"
2
+ import type { SessionState, WithParts } from "../state"
3
+ import { isMessageCompacted } from "../state/utils"
4
+ import type { UserMessage } from "@opencode-ai/sdk/v2"
5
+
6
+ const SUMMARY_ID_HASH_LENGTH = 16
7
+ const DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/dcp-message-id>)/g
8
+ const DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi
9
+ const DCP_UNPAIRED_TAG_REGEX = /<\/?dcp[^>]*>/gi
10
+
11
+ const generateStableId = (prefix: string, seed: string): string => {
12
+ const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH)
13
+ return `${prefix}_${hash}`
14
+ }
15
+
16
+ export const createSyntheticUserMessage = (
17
+ baseMessage: WithParts,
18
+ content: string,
19
+ variant?: string,
20
+ stableSeed?: string,
21
+ ): WithParts => {
22
+ const userInfo = baseMessage.info as UserMessage
23
+ const now = Date.now()
24
+ const deterministicSeed = stableSeed?.trim() || userInfo.id
25
+ const messageId = generateStableId("msg_dcp_summary", deterministicSeed)
26
+ const partId = generateStableId("prt_dcp_summary", deterministicSeed)
27
+
28
+ return {
29
+ info: {
30
+ id: messageId,
31
+ sessionID: userInfo.sessionID,
32
+ role: "user" as const,
33
+ agent: userInfo.agent,
34
+ model: userInfo.model,
35
+ time: { created: now },
36
+ ...(variant !== undefined && { variant }),
37
+ },
38
+ parts: [
39
+ {
40
+ id: partId,
41
+ sessionID: userInfo.sessionID,
42
+ messageID: messageId,
43
+ type: "text" as const,
44
+ text: content,
45
+ },
46
+ ],
47
+ }
48
+ }
49
+
50
+ export const createSyntheticTextPart = (
51
+ baseMessage: WithParts,
52
+ content: string,
53
+ stableSeed?: string,
54
+ ) => {
55
+ const userInfo = baseMessage.info as UserMessage
56
+ const deterministicSeed = stableSeed?.trim() || userInfo.id
57
+ const partId = generateStableId("prt_dcp_text", deterministicSeed)
58
+
59
+ return {
60
+ id: partId,
61
+ sessionID: userInfo.sessionID,
62
+ messageID: userInfo.id,
63
+ type: "text" as const,
64
+ text: content,
65
+ }
66
+ }
67
+
68
+ type MessagePart = WithParts["parts"][number]
69
+ type ToolPart = Extract<MessagePart, { type: "tool" }>
70
+ type TextPart = Extract<MessagePart, { type: "text" }>
71
+
72
+ export const appendToLastTextPart = (message: WithParts, injection: string): boolean => {
73
+ const textPart = findLastTextPart(message)
74
+ if (!textPart) {
75
+ return false
76
+ }
77
+
78
+ return appendToTextPart(textPart, injection)
79
+ }
80
+
81
+ const findLastTextPart = (message: WithParts): TextPart | null => {
82
+ for (let i = message.parts.length - 1; i >= 0; i--) {
83
+ const part = message.parts[i]
84
+ if (part.type === "text") {
85
+ return part
86
+ }
87
+ }
88
+
89
+ return null
90
+ }
91
+
92
+ export const appendToTextPart = (part: TextPart, injection: string): boolean => {
93
+ if (typeof part.text !== "string") {
94
+ return false
95
+ }
96
+
97
+ const normalizedInjection = injection.replace(/^\n+/, "")
98
+ if (!normalizedInjection.trim()) {
99
+ return false
100
+ }
101
+ if (part.text.includes(normalizedInjection)) {
102
+ return true
103
+ }
104
+
105
+ const baseText = part.text.replace(/\n*$/, "")
106
+ part.text = baseText.length > 0 ? `${baseText}\n\n${normalizedInjection}` : normalizedInjection
107
+ return true
108
+ }
109
+
110
+ export const appendToAllToolParts = (message: WithParts, tag: string): boolean => {
111
+ let injected = false
112
+ for (const part of message.parts) {
113
+ if (part.type === "tool") {
114
+ injected = appendToToolPart(part, tag) || injected
115
+ }
116
+ }
117
+ return injected
118
+ }
119
+
120
+ export const appendToToolPart = (part: ToolPart, tag: string): boolean => {
121
+ if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
122
+ return false
123
+ }
124
+ if (part.state.output.includes(tag)) {
125
+ return true
126
+ }
127
+
128
+ part.state.output = `${part.state.output}${tag}`
129
+ return true
130
+ }
131
+
132
+ export const hasContent = (message: WithParts): boolean => {
133
+ return message.parts.some(
134
+ (part) =>
135
+ (part.type === "text" &&
136
+ typeof part.text === "string" &&
137
+ part.text.trim().length > 0) ||
138
+ (part.type === "tool" &&
139
+ part.state?.status === "completed" &&
140
+ typeof part.state.output === "string"),
141
+ )
142
+ }
143
+
144
+ export function buildToolIdList(state: SessionState, messages: WithParts[]): string[] {
145
+ const toolIds: string[] = []
146
+ for (const msg of messages) {
147
+ if (isMessageCompacted(state, msg)) {
148
+ continue
149
+ }
150
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
151
+ if (parts.length > 0) {
152
+ for (const part of parts) {
153
+ if (part.type === "tool" && part.callID && part.tool) {
154
+ toolIds.push(part.callID)
155
+ }
156
+ }
157
+ }
158
+ }
159
+ state.toolIdList = toolIds
160
+ return toolIds
161
+ }
162
+
163
+ export const replaceBlockIdsWithBlocked = (text: string): string => {
164
+ return text.replace(DCP_BLOCK_ID_TAG_REGEX, "$1BLOCKED$2")
165
+ }
166
+
167
+ export const stripHallucinationsFromString = (text: string): string => {
168
+ return text.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "")
169
+ }
170
+
171
+ export const stripHallucinations = (messages: WithParts[]): void => {
172
+ for (const message of messages) {
173
+ for (const part of message.parts) {
174
+ if (part.type === "text" && typeof part.text === "string") {
175
+ part.text = stripHallucinationsFromString(part.text)
176
+ }
177
+
178
+ if (
179
+ part.type === "tool" &&
180
+ part.state?.status === "completed" &&
181
+ typeof part.state.output === "string"
182
+ ) {
183
+ part.state.output = stripHallucinationsFromString(part.state.output)
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,42 @@
1
+ export const COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
2
+
3
+ THE SUMMARY
4
+ 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.
5
+
6
+ USER INTENT FIDELITY
7
+ When a selected message contains user intent, preserve that intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
8
+ Directly quote short user instructions when that best preserves exact meaning.
9
+
10
+ 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.
11
+ If a message contains no significant technical decisions, code changes, or user requirements, produce a minimal one-line summary rather than a detailed one.
12
+
13
+ MESSAGE IDS
14
+ You specify individual raw messages by ID using the injected IDs visible in the conversation:
15
+
16
+ - \`mNNNN\` IDs identify raw messages
17
+
18
+ Each message has an ID inside XML metadata tags like \`<dcp-message-id priority="high">m0007</dcp-message-id>\`.
19
+ The same ID tag appears in every tool output of the message it belongs to — each unique ID identifies one complete message.
20
+ Treat these tags as message metadata only, not as content to summarize. Use only the inner \`mNNNN\` value as the \`messageId\`.
21
+ The \`priority\` attribute indicates relative context cost. You MUST compress high-priority messages when their full text is no longer necessary for the active task.
22
+ If prior compress-tool results are present, always compress and summarize them minimally only as part of a broader compression pass. Do not invoke the compress tool solely to re-compress an earlier compression result.
23
+ Messages marked as \`<dcp-message-id>BLOCKED</dcp-message-id>\` cannot be compressed.
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
+ - Ignore XML attributes such as \`priority\` when copying the ID; use only the inner \`mNNNN\` value.
30
+ - Do not invent IDs. Use only IDs that are present in context.
31
+
32
+ BATCHING
33
+ Select MANY messages in a single tool call when they are safe to compress.
34
+ Each entry should summarize exactly one message, and the tool can receive as many entries as needed in one batch.
35
+
36
+ GENERAL CLEANUP
37
+ Use the topic "general cleanup" for broad cleanup passes.
38
+ During general cleanup, compress all medium and high-priority messages that are not relevant to the active task.
39
+ Optimize for reducing context footprint, not for grouping messages by topic.
40
+ Do not compress away still-active instructions, unresolved questions, or constraints that are likely to matter soon.
41
+ Prioritize the earliest messages in the context as they will be the least relevant to the active task.
42
+ `