@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.0

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 (209) hide show
  1. package/LICENSE +619 -0
  2. package/README.md +348 -2
  3. package/dist/index.d.ts +7 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +7611 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/commands/compression-targets.d.ts +13 -0
  8. package/dist/lib/commands/compression-targets.d.ts.map +1 -0
  9. package/dist/lib/commands/context.d.ts +67 -0
  10. package/dist/lib/commands/context.d.ts.map +1 -0
  11. package/dist/lib/commands/decompress.d.ts +12 -0
  12. package/dist/lib/commands/decompress.d.ts.map +1 -0
  13. package/dist/lib/commands/help.d.ts +18 -0
  14. package/dist/lib/commands/help.d.ts.map +1 -0
  15. package/dist/lib/commands/index.d.ts +8 -0
  16. package/dist/lib/commands/index.d.ts.map +1 -0
  17. package/dist/lib/commands/manual.d.ts +23 -0
  18. package/dist/lib/commands/manual.d.ts.map +1 -0
  19. package/dist/lib/commands/recompress.d.ts +12 -0
  20. package/dist/lib/commands/recompress.d.ts.map +1 -0
  21. package/dist/lib/commands/stats.d.ts +25 -0
  22. package/dist/lib/commands/stats.d.ts.map +1 -0
  23. package/dist/lib/commands/sweep.d.ts +23 -0
  24. package/dist/lib/commands/sweep.d.ts.map +1 -0
  25. package/dist/lib/compress/index.d.ts +3 -0
  26. package/dist/lib/compress/index.d.ts.map +1 -0
  27. package/dist/lib/compress/pipeline.d.ts +29 -0
  28. package/dist/lib/compress/pipeline.d.ts.map +1 -0
  29. package/dist/lib/compress/protected-content.d.ts +6 -0
  30. package/dist/lib/compress/protected-content.d.ts.map +1 -0
  31. package/dist/lib/compress/rc.d.ts +15 -0
  32. package/dist/lib/compress/rc.d.ts.map +1 -0
  33. package/dist/lib/compress/search.d.ts +12 -0
  34. package/dist/lib/compress/search.d.ts.map +1 -0
  35. package/dist/lib/compress/state.d.ts +10 -0
  36. package/dist/lib/compress/state.d.ts.map +1 -0
  37. package/dist/lib/compress/summary.d.ts +58 -0
  38. package/dist/lib/compress/summary.d.ts.map +1 -0
  39. package/dist/lib/compress/timing.d.ts +18 -0
  40. package/dist/lib/compress/timing.d.ts.map +1 -0
  41. package/dist/lib/compress/types.d.ts +55 -0
  42. package/dist/lib/compress/types.d.ts.map +1 -0
  43. package/dist/lib/compress-permission.d.ts +4 -0
  44. package/dist/lib/compress-permission.d.ts.map +1 -0
  45. package/dist/lib/config.d.ts +83 -0
  46. package/dist/lib/config.d.ts.map +1 -0
  47. package/dist/lib/hooks.d.ts +22 -0
  48. package/dist/lib/hooks.d.ts.map +1 -0
  49. package/dist/lib/logger.d.ts +31 -0
  50. package/dist/lib/logger.d.ts.map +1 -0
  51. package/dist/lib/message-ids.d.ts +20 -0
  52. package/dist/lib/message-ids.d.ts.map +1 -0
  53. package/dist/lib/messages/index.d.ts +8 -0
  54. package/dist/lib/messages/index.d.ts.map +1 -0
  55. package/dist/lib/messages/inject/inject.d.ts +7 -0
  56. package/dist/lib/messages/inject/inject.d.ts.map +1 -0
  57. package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
  58. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
  59. package/dist/lib/messages/inject/utils.d.ts +23 -0
  60. package/dist/lib/messages/inject/utils.d.ts.map +1 -0
  61. package/dist/lib/messages/prune.d.ts +4 -0
  62. package/dist/lib/messages/prune.d.ts.map +1 -0
  63. package/dist/lib/messages/query.d.ts +7 -0
  64. package/dist/lib/messages/query.d.ts.map +1 -0
  65. package/dist/lib/messages/reasoning-strip.d.ts +8 -0
  66. package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
  67. package/dist/lib/messages/shape.d.ts +5 -0
  68. package/dist/lib/messages/shape.d.ts.map +1 -0
  69. package/dist/lib/messages/sync.d.ts +4 -0
  70. package/dist/lib/messages/sync.d.ts.map +1 -0
  71. package/dist/lib/messages/utils.d.ts +27 -0
  72. package/dist/lib/messages/utils.d.ts.map +1 -0
  73. package/dist/lib/prompts/compress-rc.d.ts +4 -0
  74. package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
  75. package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
  76. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
  77. package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
  78. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  79. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  80. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  81. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  82. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  83. package/dist/lib/prompts/index.d.ts +4 -0
  84. package/dist/lib/prompts/index.d.ts.map +1 -0
  85. package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
  86. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
  87. package/dist/lib/prompts/store.d.ts +26 -0
  88. package/dist/lib/prompts/store.d.ts.map +1 -0
  89. package/dist/lib/prompts/system.d.ts +4 -0
  90. package/dist/lib/prompts/system.d.ts.map +1 -0
  91. package/dist/lib/prompts/turn-nudge.d.ts +2 -0
  92. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
  93. package/dist/lib/protected-patterns.d.ts +6 -0
  94. package/dist/lib/protected-patterns.d.ts.map +1 -0
  95. package/dist/lib/state/index.d.ts +5 -0
  96. package/dist/lib/state/index.d.ts.map +1 -0
  97. package/dist/lib/state/persistence.d.ts +45 -0
  98. package/dist/lib/state/persistence.d.ts.map +1 -0
  99. package/dist/lib/state/state.d.ts +9 -0
  100. package/dist/lib/state/state.d.ts.map +1 -0
  101. package/dist/lib/state/tool-cache.d.ts +13 -0
  102. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  103. package/dist/lib/state/types.d.ts +102 -0
  104. package/dist/lib/state/types.d.ts.map +1 -0
  105. package/dist/lib/state/utils.d.ts +22 -0
  106. package/dist/lib/state/utils.d.ts.map +1 -0
  107. package/dist/lib/strategies/deduplication.d.ts +10 -0
  108. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  109. package/dist/lib/strategies/index.d.ts +3 -0
  110. package/dist/lib/strategies/index.d.ts.map +1 -0
  111. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  112. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  113. package/dist/lib/subagents/subagent-results.d.ts +5 -0
  114. package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
  115. package/dist/lib/token-utils.d.ts +19 -0
  116. package/dist/lib/token-utils.d.ts.map +1 -0
  117. package/dist/lib/tui/dialogs.d.ts +29 -0
  118. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  119. package/dist/lib/tui/format.d.ts +4 -0
  120. package/dist/lib/tui/format.d.ts.map +1 -0
  121. package/dist/lib/tui/types.d.ts +18 -0
  122. package/dist/lib/tui/types.d.ts.map +1 -0
  123. package/dist/lib/tui/ui.d.ts +48 -0
  124. package/dist/lib/tui/ui.d.ts.map +1 -0
  125. package/dist/lib/ui/notification.d.ts +17 -0
  126. package/dist/lib/ui/notification.d.ts.map +1 -0
  127. package/dist/lib/ui/utils.d.ts +10 -0
  128. package/dist/lib/ui/utils.d.ts.map +1 -0
  129. package/dist/lib/v2/index.d.ts +5 -0
  130. package/dist/lib/v2/index.d.ts.map +1 -0
  131. package/dist/lib/v2/messages.d.ts +13 -0
  132. package/dist/lib/v2/messages.d.ts.map +1 -0
  133. package/dist/lib/v2/rpc.d.ts +55 -0
  134. package/dist/lib/v2/rpc.d.ts.map +1 -0
  135. package/dist/lib/v2/theme.d.ts +55 -0
  136. package/dist/lib/v2/theme.d.ts.map +1 -0
  137. package/dist/lib/v2/tui.d.ts +4 -0
  138. package/dist/lib/v2/tui.d.ts.map +1 -0
  139. package/dist/tui.d.ts +7 -0
  140. package/dist/tui.d.ts.map +1 -0
  141. package/lib/commands/compression-targets.ts +54 -0
  142. package/lib/commands/context.ts +305 -0
  143. package/lib/commands/decompress.ts +273 -0
  144. package/lib/commands/help.ts +76 -0
  145. package/lib/commands/index.ts +11 -0
  146. package/lib/commands/manual.ts +126 -0
  147. package/lib/commands/recompress.ts +222 -0
  148. package/lib/commands/stats.ts +159 -0
  149. package/lib/commands/sweep.ts +266 -0
  150. package/lib/compress/index.ts +2 -0
  151. package/lib/compress/pipeline.ts +116 -0
  152. package/lib/compress/protected-content.ts +163 -0
  153. package/lib/compress/rc.ts +458 -0
  154. package/lib/compress/search.ts +277 -0
  155. package/lib/compress/state.ts +272 -0
  156. package/lib/compress/summary.ts +293 -0
  157. package/lib/compress/timing.ts +77 -0
  158. package/lib/compress/types.ts +57 -0
  159. package/lib/compress-permission.ts +9 -0
  160. package/lib/config.ts +995 -0
  161. package/lib/hooks.ts +205 -0
  162. package/lib/logger.ts +226 -0
  163. package/lib/message-ids.ts +181 -0
  164. package/lib/messages/index.ts +7 -0
  165. package/lib/messages/inject/inject.ts +215 -0
  166. package/lib/messages/inject/subagent-results.ts +84 -0
  167. package/lib/messages/inject/utils.ts +304 -0
  168. package/lib/messages/prune.ts +238 -0
  169. package/lib/messages/query.ts +71 -0
  170. package/lib/messages/reasoning-strip.ts +40 -0
  171. package/lib/messages/shape.ts +50 -0
  172. package/lib/messages/sync.ts +124 -0
  173. package/lib/messages/utils.ts +190 -0
  174. package/lib/prompts/compress-rc.ts +25 -0
  175. package/lib/prompts/context-limit-nudge.ts +18 -0
  176. package/lib/prompts/extensions/nudge.ts +34 -0
  177. package/lib/prompts/extensions/system.ts +32 -0
  178. package/lib/prompts/extensions/tool.ts +21 -0
  179. package/lib/prompts/index.ts +29 -0
  180. package/lib/prompts/iteration-nudge.ts +6 -0
  181. package/lib/prompts/store.ts +452 -0
  182. package/lib/prompts/system.ts +41 -0
  183. package/lib/prompts/turn-nudge.ts +10 -0
  184. package/lib/protected-patterns.ts +159 -0
  185. package/lib/state/index.ts +4 -0
  186. package/lib/state/persistence.ts +305 -0
  187. package/lib/state/state.ts +210 -0
  188. package/lib/state/tool-cache.ts +99 -0
  189. package/lib/state/types.ts +114 -0
  190. package/lib/state/utils.ts +345 -0
  191. package/lib/strategies/deduplication.ts +125 -0
  192. package/lib/strategies/index.ts +2 -0
  193. package/lib/strategies/purge-errors.ts +86 -0
  194. package/lib/subagents/subagent-results.ts +75 -0
  195. package/lib/token-utils.ts +164 -0
  196. package/lib/tui/dialogs.tsx +237 -0
  197. package/lib/tui/format.ts +25 -0
  198. package/lib/tui/types.ts +25 -0
  199. package/lib/tui/ui.tsx +245 -0
  200. package/lib/ui/notification.ts +347 -0
  201. package/lib/ui/utils.ts +304 -0
  202. package/lib/v2/index.ts +401 -0
  203. package/lib/v2/messages.ts +252 -0
  204. package/lib/v2/rpc.ts +48 -0
  205. package/lib/v2/theme.ts +67 -0
  206. package/lib/v2/tui.tsx +115 -0
  207. package/package.json +91 -5
  208. package/server.js +1 -0
  209. package/tui.tsx +7 -0
@@ -0,0 +1,238 @@
1
+ import type { SessionState, WithParts } from "../state"
2
+ import type { Logger } from "../logger"
3
+ import { isMessageCompacted } from "../state/utils"
4
+ import { createSyntheticUserMessage } from "./utils"
5
+ import { getLastUserMessage } from "./query"
6
+ import type { UserMessage } from "@opencode-ai/sdk/v2"
7
+ import { formatBlockRef } from "../message-ids"
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
+ messages: WithParts[],
18
+ summaryBase?: WithParts,
19
+ ): void => {
20
+ filterCompressedRanges(state, logger, messages, summaryBase)
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
+ messages: WithParts[],
163
+ summaryBase?: 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) ?? summaryBase
196
+
197
+ if (userMessage) {
198
+ const userInfo = userMessage.info as UserMessage
199
+ // Persisted summaries can outlive the host's tag format.
200
+ let renderedSummary = rawSummaryContent
201
+ if (state.idFormat === "compact") {
202
+ renderedSummary = renderedSummary.replace(
203
+ /<dcp-message-id>b(\d+)<\/dcp-message-id>\s*$/i,
204
+ (_, id) => formatBlockRef(Number(id), "compact"),
205
+ )
206
+ }
207
+ const summaryContent = renderedSummary
208
+ const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`
209
+ result.push(
210
+ createSyntheticUserMessage(userMessage, summaryContent, summarySeed),
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,71 @@
1
+ import type { PluginConfig } from "../config"
2
+ import type { WithParts } from "../state"
3
+ import { isMessageWithInfo } from "./shape"
4
+
5
+ export const getLastUserMessage = (
6
+ messages: WithParts[],
7
+ startIndex?: number,
8
+ ): WithParts | null => {
9
+ const start = startIndex ?? messages.length - 1
10
+ for (let i = start; i >= 0; i--) {
11
+ const msg = messages[i]
12
+ if (!isMessageWithInfo(msg)) {
13
+ continue
14
+ }
15
+ if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
16
+ return msg
17
+ }
18
+ }
19
+ return null
20
+ }
21
+
22
+ export const messageHasCompress = (message: WithParts): boolean => {
23
+ if (!isMessageWithInfo(message)) {
24
+ return false
25
+ }
26
+
27
+ if (message.info.role !== "assistant") {
28
+ return false
29
+ }
30
+
31
+ const parts = Array.isArray(message.parts) ? message.parts : []
32
+ return parts.some(
33
+ (part) =>
34
+ part.type === "tool" && part.tool === "compress" && part.state?.status === "completed",
35
+ )
36
+ }
37
+
38
+ export const isIgnoredUserMessage = (message: WithParts): boolean => {
39
+ if (!isMessageWithInfo(message)) {
40
+ return false
41
+ }
42
+
43
+ if (message.info.role !== "user") {
44
+ return false
45
+ }
46
+
47
+ const parts = Array.isArray(message.parts) ? message.parts : []
48
+ if (parts.length === 0) {
49
+ return true
50
+ }
51
+
52
+ for (const part of parts) {
53
+ if (!(part as any).ignored) {
54
+ return false
55
+ }
56
+ }
57
+
58
+ return true
59
+ }
60
+
61
+ export function isProtectedUserMessage(config: PluginConfig, message: WithParts): boolean {
62
+ if (!isMessageWithInfo(message)) {
63
+ return false
64
+ }
65
+
66
+ return (
67
+ config.compress.protectUserMessages &&
68
+ message.info.role === "user" &&
69
+ !isIgnoredUserMessage(message)
70
+ )
71
+ }
@@ -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,50 @@
1
+ import type { WithParts } from "../state"
2
+
3
+ export function isMessageWithInfo(message: unknown): message is WithParts {
4
+ if (!message || typeof message !== "object") {
5
+ return false
6
+ }
7
+
8
+ const info = (message as any).info
9
+ const parts = (message as any).parts
10
+ if (!info || typeof info !== "object") {
11
+ return false
12
+ }
13
+
14
+ return (
15
+ typeof info.id === "string" &&
16
+ info.id.length > 0 &&
17
+ typeof info.sessionID === "string" &&
18
+ info.sessionID.length > 0 &&
19
+ (info.role === "user" || info.role === "assistant") &&
20
+ info.time &&
21
+ typeof info.time === "object" &&
22
+ typeof info.time.created === "number" &&
23
+ Array.isArray(parts)
24
+ )
25
+ }
26
+
27
+ export function filterMessages(messages: unknown): WithParts[] {
28
+ if (!Array.isArray(messages)) {
29
+ return []
30
+ }
31
+
32
+ return messages.filter(isMessageWithInfo)
33
+ }
34
+
35
+ export function filterMessagesInPlace(messages: unknown): WithParts[] {
36
+ if (!Array.isArray(messages)) {
37
+ return []
38
+ }
39
+
40
+ let writeIndex = 0
41
+
42
+ for (const message of messages) {
43
+ if (isMessageWithInfo(message)) {
44
+ messages[writeIndex++] = message
45
+ }
46
+ }
47
+
48
+ messages.length = writeIndex
49
+ return messages as WithParts[]
50
+ }
@@ -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
+ }