@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,277 @@
1
+ import type { SessionState, WithParts } from "../state"
2
+ import type { PluginConfig } from "../config"
3
+ import { formatBlockRef, parseBoundaryId } from "../message-ids"
4
+ import { isIgnoredUserMessage, isProtectedUserMessage } from "../messages/query"
5
+ import { filterMessages } from "../messages/shape"
6
+ import { countAllMessageTokens } from "../token-utils"
7
+ import type { BoundaryReference, SearchContext, SelectionResolution } from "./types"
8
+
9
+ export async function fetchSessionMessages(client: any, sessionId: string): Promise<WithParts[]> {
10
+ const response = await client.session.messages({
11
+ path: { id: sessionId },
12
+ })
13
+
14
+ return filterMessages(response?.data || response)
15
+ }
16
+
17
+ export function buildSearchContext(state: SessionState, rawMessages: WithParts[]): SearchContext {
18
+ const rawMessagesById = new Map<string, WithParts>()
19
+ const rawIndexById = new Map<string, number>()
20
+ for (const msg of rawMessages) {
21
+ rawMessagesById.set(msg.info.id, msg)
22
+ }
23
+ for (let index = 0; index < rawMessages.length; index++) {
24
+ const message = rawMessages[index]
25
+ if (!message) {
26
+ continue
27
+ }
28
+ rawIndexById.set(message.info.id, index)
29
+ }
30
+
31
+ const summaryByBlockId = new Map()
32
+ for (const [blockId, block] of state.prune.messages.blocksById) {
33
+ if (!block.active) {
34
+ continue
35
+ }
36
+ summaryByBlockId.set(blockId, block)
37
+ }
38
+
39
+ return {
40
+ rawMessages,
41
+ rawMessagesById,
42
+ rawIndexById,
43
+ summaryByBlockId,
44
+ }
45
+ }
46
+
47
+ export function resolveBoundaryIds(
48
+ context: SearchContext,
49
+ state: SessionState,
50
+ config: PluginConfig,
51
+ startId: string,
52
+ endId: string,
53
+ ): { startReference: BoundaryReference; endReference: BoundaryReference } {
54
+ const lookup = buildBoundaryLookup(context, state, config)
55
+ const issues: string[] = []
56
+ const parsedStartId = parseBoundaryId(startId, state.idFormat)
57
+ const parsedEndId = parseBoundaryId(endId, state.idFormat)
58
+ const messageExample = state.idFormat === "compact" ? "@4@" : "mNNNN"
59
+ const blockExample = state.idFormat === "compact" ? "@b1@" : "bN"
60
+
61
+ if (parsedStartId === null) {
62
+ issues.push(
63
+ `startId is invalid. Use an injected message ID (${messageExample}) or block ID (${blockExample}).`,
64
+ )
65
+ }
66
+
67
+ if (parsedEndId === null) {
68
+ issues.push(
69
+ `endId is invalid. Use an injected message ID (${messageExample}) or block ID (${blockExample}).`,
70
+ )
71
+ }
72
+
73
+ if (issues.length > 0) {
74
+ throw new Error(
75
+ issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n"),
76
+ )
77
+ }
78
+
79
+ if (!parsedStartId || !parsedEndId) {
80
+ throw new Error("Invalid boundary ID(s)")
81
+ }
82
+
83
+ const startReference = lookup.get(parsedStartId.ref)
84
+ const endReference = lookup.get(parsedEndId.ref)
85
+
86
+ if (!startReference) {
87
+ issues.push(
88
+ `startId ${parsedStartId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`,
89
+ )
90
+ }
91
+
92
+ if (!endReference) {
93
+ issues.push(
94
+ `endId ${parsedEndId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`,
95
+ )
96
+ }
97
+
98
+ if (issues.length > 0) {
99
+ throw new Error(
100
+ issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n"),
101
+ )
102
+ }
103
+
104
+ if (!startReference || !endReference) {
105
+ throw new Error("Failed to resolve boundary IDs")
106
+ }
107
+
108
+ if (startReference.rawIndex > endReference.rawIndex) {
109
+ throw new Error(
110
+ `startId ${parsedStartId.ref} appears after endId ${parsedEndId.ref} in the conversation. Start must come before end.`,
111
+ )
112
+ }
113
+
114
+ return { startReference, endReference }
115
+ }
116
+
117
+ export function resolveSelection(
118
+ context: SearchContext,
119
+ config: PluginConfig,
120
+ startReference: BoundaryReference,
121
+ endReference: BoundaryReference,
122
+ ): SelectionResolution {
123
+ const startRawIndex = startReference.rawIndex
124
+ const endRawIndex = endReference.rawIndex
125
+ const messageIds: string[] = []
126
+ const messageSeen = new Set<string>()
127
+ const toolIds: string[] = []
128
+ const toolSeen = new Set<string>()
129
+ const requiredBlockIds: number[] = []
130
+ const requiredBlockSeen = new Set<number>()
131
+ const messageTokenById = new Map<string, number>()
132
+
133
+ for (let index = startRawIndex; index <= endRawIndex; index++) {
134
+ const rawMessage = context.rawMessages[index]
135
+ if (!rawMessage) {
136
+ continue
137
+ }
138
+ if (isIgnoredUserMessage(rawMessage) || isProtectedUserMessage(config, rawMessage)) {
139
+ continue
140
+ }
141
+
142
+ const messageId = rawMessage.info.id
143
+ if (!messageSeen.has(messageId)) {
144
+ messageSeen.add(messageId)
145
+ messageIds.push(messageId)
146
+ }
147
+
148
+ if (!messageTokenById.has(messageId)) {
149
+ messageTokenById.set(messageId, countAllMessageTokens(rawMessage))
150
+ }
151
+
152
+ const parts = Array.isArray(rawMessage.parts) ? rawMessage.parts : []
153
+ for (const part of parts) {
154
+ if (part.type !== "tool" || !part.callID) {
155
+ continue
156
+ }
157
+ if (toolSeen.has(part.callID)) {
158
+ continue
159
+ }
160
+ toolSeen.add(part.callID)
161
+ toolIds.push(part.callID)
162
+ }
163
+ }
164
+
165
+ const selectedMessageIds = new Set(messageIds)
166
+ const summariesInSelection: Array<{ blockId: number; rawIndex: number }> = []
167
+ for (const summary of context.summaryByBlockId.values()) {
168
+ if (!selectedMessageIds.has(summary.anchorMessageId)) {
169
+ continue
170
+ }
171
+
172
+ const anchorIndex = context.rawIndexById.get(summary.anchorMessageId)
173
+ if (anchorIndex === undefined) {
174
+ continue
175
+ }
176
+
177
+ summariesInSelection.push({
178
+ blockId: summary.blockId,
179
+ rawIndex: anchorIndex,
180
+ })
181
+ }
182
+
183
+ summariesInSelection.sort((a, b) => a.rawIndex - b.rawIndex || a.blockId - b.blockId)
184
+ for (const summary of summariesInSelection) {
185
+ if (requiredBlockSeen.has(summary.blockId)) {
186
+ continue
187
+ }
188
+ requiredBlockSeen.add(summary.blockId)
189
+ requiredBlockIds.push(summary.blockId)
190
+ }
191
+
192
+ if (messageIds.length === 0) {
193
+ throw new Error(
194
+ "Failed to map boundary matches back to raw messages. Choose boundaries that include original conversation messages.",
195
+ )
196
+ }
197
+
198
+ return {
199
+ startReference,
200
+ endReference,
201
+ messageIds,
202
+ messageTokenById,
203
+ toolIds,
204
+ requiredBlockIds,
205
+ }
206
+ }
207
+
208
+ export function resolveAnchorMessageId(startReference: BoundaryReference): string {
209
+ if (startReference.kind === "compressed-block") {
210
+ if (!startReference.anchorMessageId) {
211
+ throw new Error("Failed to map boundary matches back to raw messages")
212
+ }
213
+ return startReference.anchorMessageId
214
+ }
215
+
216
+ if (!startReference.messageId) {
217
+ throw new Error("Failed to map boundary matches back to raw messages")
218
+ }
219
+ return startReference.messageId
220
+ }
221
+
222
+ function buildBoundaryLookup(
223
+ context: SearchContext,
224
+ state: SessionState,
225
+ config: PluginConfig,
226
+ ): Map<string, BoundaryReference> {
227
+ const lookup = new Map<string, BoundaryReference>()
228
+
229
+ for (const [messageRef, messageId] of state.messageIds.byRef) {
230
+ const rawMessage = context.rawMessagesById.get(messageId)
231
+ if (!rawMessage) {
232
+ continue
233
+ }
234
+ if (isIgnoredUserMessage(rawMessage) || isProtectedUserMessage(config, rawMessage)) {
235
+ continue
236
+ }
237
+
238
+ const rawIndex = context.rawIndexById.get(messageId)
239
+ if (rawIndex === undefined) {
240
+ continue
241
+ }
242
+ lookup.set(messageRef, {
243
+ kind: "message",
244
+ rawIndex,
245
+ messageId,
246
+ })
247
+ }
248
+
249
+ const summaries = Array.from(context.summaryByBlockId.values()).sort(
250
+ (a, b) => a.blockId - b.blockId,
251
+ )
252
+ for (const summary of summaries) {
253
+ const anchorMessage = context.rawMessagesById.get(summary.anchorMessageId)
254
+ if (!anchorMessage) {
255
+ continue
256
+ }
257
+ if (isIgnoredUserMessage(anchorMessage)) {
258
+ continue
259
+ }
260
+
261
+ const rawIndex = context.rawIndexById.get(summary.anchorMessageId)
262
+ if (rawIndex === undefined) {
263
+ continue
264
+ }
265
+ const blockRef = formatBlockRef(summary.blockId, state.idFormat)
266
+ if (!lookup.has(blockRef)) {
267
+ lookup.set(blockRef, {
268
+ kind: "compressed-block",
269
+ rawIndex,
270
+ blockId: summary.blockId,
271
+ anchorMessageId: summary.anchorMessageId,
272
+ })
273
+ }
274
+ }
275
+
276
+ return lookup
277
+ }
@@ -0,0 +1,272 @@
1
+ import type { CompressionBlock, PruneMessagesState, SessionState } from "../state"
2
+ import { formatBlockRef, formatMessageIdTag, type IdFormat } from "../message-ids"
3
+ import type { AppliedCompressionResult, CompressionStateInput, SelectionResolution } from "./types"
4
+
5
+ export const COMPRESSED_BLOCK_HEADER = "[Compressed conversation section]"
6
+
7
+ export function allocateBlockId(state: SessionState): number {
8
+ const next = state.prune.messages.nextBlockId
9
+ if (!Number.isInteger(next) || next < 1) {
10
+ state.prune.messages.nextBlockId = 2
11
+ return 1
12
+ }
13
+
14
+ state.prune.messages.nextBlockId = next + 1
15
+ return next
16
+ }
17
+
18
+ export function allocateRunId(state: SessionState): number {
19
+ const next = state.prune.messages.nextRunId
20
+ if (!Number.isInteger(next) || next < 1) {
21
+ state.prune.messages.nextRunId = 2
22
+ return 1
23
+ }
24
+
25
+ state.prune.messages.nextRunId = next + 1
26
+ return next
27
+ }
28
+
29
+ export function attachCompressionDuration(
30
+ messagesState: PruneMessagesState,
31
+ messageId: string,
32
+ callId: string,
33
+ durationMs: number,
34
+ ): number {
35
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs)) {
36
+ return 0
37
+ }
38
+
39
+ let updates = 0
40
+ for (const block of messagesState.blocksById.values()) {
41
+ if (block.compressMessageId !== messageId || block.compressCallId !== callId) {
42
+ continue
43
+ }
44
+
45
+ block.durationMs = durationMs
46
+ updates++
47
+ }
48
+
49
+ return updates
50
+ }
51
+
52
+ export function wrapCompressedSummary(
53
+ blockId: number,
54
+ summary: string,
55
+ format: IdFormat = "xml",
56
+ ): string {
57
+ const header = COMPRESSED_BLOCK_HEADER
58
+ const footer = formatMessageIdTag(formatBlockRef(blockId, format), undefined, format)
59
+ const body = summary.trim()
60
+ if (body.length === 0) {
61
+ return `${header}\n${footer}`
62
+ }
63
+ return `${header}\n${body}\n\n${footer}`
64
+ }
65
+
66
+ export function applyCompressionState(
67
+ state: SessionState,
68
+ input: CompressionStateInput,
69
+ selection: SelectionResolution,
70
+ anchorMessageId: string,
71
+ blockId: number,
72
+ summary: string,
73
+ consumedBlockIds: number[],
74
+ ): AppliedCompressionResult {
75
+ const messagesState = state.prune.messages
76
+ const consumed = [...new Set(consumedBlockIds.filter((id) => Number.isInteger(id) && id > 0))]
77
+ const included = [...consumed]
78
+
79
+ const effectiveMessageIds = new Set<string>(selection.messageIds)
80
+ const effectiveToolIds = new Set<string>(selection.toolIds)
81
+
82
+ for (const consumedBlockId of consumed) {
83
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId)
84
+ if (!consumedBlock) {
85
+ continue
86
+ }
87
+ for (const messageId of consumedBlock.effectiveMessageIds) {
88
+ effectiveMessageIds.add(messageId)
89
+ }
90
+ for (const toolId of consumedBlock.effectiveToolIds) {
91
+ effectiveToolIds.add(toolId)
92
+ }
93
+ }
94
+
95
+ const initiallyActiveMessages = new Set<string>()
96
+ for (const messageId of effectiveMessageIds) {
97
+ const entry = messagesState.byMessageId.get(messageId)
98
+ if (entry && entry.activeBlockIds.length > 0) {
99
+ initiallyActiveMessages.add(messageId)
100
+ }
101
+ }
102
+
103
+ const initiallyActiveToolIds = new Set<string>()
104
+ for (const activeBlockId of messagesState.activeBlockIds) {
105
+ const activeBlock = messagesState.blocksById.get(activeBlockId)
106
+ if (!activeBlock || !activeBlock.active) {
107
+ continue
108
+ }
109
+
110
+ for (const toolId of activeBlock.effectiveToolIds) {
111
+ initiallyActiveToolIds.add(toolId)
112
+ }
113
+ }
114
+
115
+ const createdAt = Date.now()
116
+ const block: CompressionBlock = {
117
+ blockId,
118
+ runId: input.runId,
119
+ active: true,
120
+ deactivatedByUser: false,
121
+ compressedTokens: 0,
122
+ summaryTokens: input.summaryTokens,
123
+ durationMs: 0,
124
+ mode: input.mode,
125
+ topic: input.topic,
126
+ batchTopic: input.batchTopic,
127
+ startId: input.startId,
128
+ endId: input.endId,
129
+ anchorMessageId,
130
+ compressMessageId: input.compressMessageId,
131
+ compressCallId: input.compressCallId,
132
+ includedBlockIds: included,
133
+ consumedBlockIds: consumed,
134
+ parentBlockIds: [],
135
+ directMessageIds: [],
136
+ directToolIds: [],
137
+ effectiveMessageIds: [...effectiveMessageIds],
138
+ effectiveToolIds: [...effectiveToolIds],
139
+ createdAt,
140
+ summary,
141
+ }
142
+
143
+ messagesState.blocksById.set(blockId, block)
144
+ messagesState.activeBlockIds.add(blockId)
145
+ messagesState.activeByAnchorMessageId.set(anchorMessageId, blockId)
146
+
147
+ const deactivatedAt = Date.now()
148
+ for (const consumedBlockId of consumed) {
149
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId)
150
+ if (!consumedBlock || !consumedBlock.active) {
151
+ continue
152
+ }
153
+
154
+ consumedBlock.active = false
155
+ consumedBlock.deactivatedAt = deactivatedAt
156
+ consumedBlock.deactivatedByBlockId = blockId
157
+ if (!consumedBlock.parentBlockIds.includes(blockId)) {
158
+ consumedBlock.parentBlockIds.push(blockId)
159
+ }
160
+
161
+ messagesState.activeBlockIds.delete(consumedBlockId)
162
+ const mappedBlockId = messagesState.activeByAnchorMessageId.get(
163
+ consumedBlock.anchorMessageId,
164
+ )
165
+ if (mappedBlockId === consumedBlockId) {
166
+ messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId)
167
+ }
168
+ }
169
+
170
+ const removeActiveBlockId = (
171
+ entry: { activeBlockIds: number[] },
172
+ blockIdToRemove: number,
173
+ ): void => {
174
+ if (entry.activeBlockIds.length === 0) {
175
+ return
176
+ }
177
+ entry.activeBlockIds = entry.activeBlockIds.filter((id) => id !== blockIdToRemove)
178
+ }
179
+
180
+ for (const consumedBlockId of consumed) {
181
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId)
182
+ if (!consumedBlock) {
183
+ continue
184
+ }
185
+ for (const messageId of consumedBlock.effectiveMessageIds) {
186
+ const entry = messagesState.byMessageId.get(messageId)
187
+ if (!entry) {
188
+ continue
189
+ }
190
+ removeActiveBlockId(entry, consumedBlockId)
191
+ }
192
+ }
193
+
194
+ for (const messageId of selection.messageIds) {
195
+ const tokenCount = selection.messageTokenById.get(messageId) || 0
196
+ const existing = messagesState.byMessageId.get(messageId)
197
+
198
+ if (!existing) {
199
+ messagesState.byMessageId.set(messageId, {
200
+ tokenCount,
201
+ allBlockIds: [blockId],
202
+ activeBlockIds: [blockId],
203
+ })
204
+ continue
205
+ }
206
+
207
+ existing.tokenCount = Math.max(existing.tokenCount, tokenCount)
208
+ if (!existing.allBlockIds.includes(blockId)) {
209
+ existing.allBlockIds.push(blockId)
210
+ }
211
+ if (!existing.activeBlockIds.includes(blockId)) {
212
+ existing.activeBlockIds.push(blockId)
213
+ }
214
+ }
215
+
216
+ for (const messageId of block.effectiveMessageIds) {
217
+ if (selection.messageTokenById.has(messageId)) {
218
+ continue
219
+ }
220
+
221
+ const existing = messagesState.byMessageId.get(messageId)
222
+ if (!existing) {
223
+ continue
224
+ }
225
+ if (!existing.allBlockIds.includes(blockId)) {
226
+ existing.allBlockIds.push(blockId)
227
+ }
228
+ if (!existing.activeBlockIds.includes(blockId)) {
229
+ existing.activeBlockIds.push(blockId)
230
+ }
231
+ }
232
+
233
+ let compressedTokens = 0
234
+ const newlyCompressedMessageIds: string[] = []
235
+ for (const messageId of effectiveMessageIds) {
236
+ const entry = messagesState.byMessageId.get(messageId)
237
+ if (!entry) {
238
+ continue
239
+ }
240
+
241
+ const isNowActive = entry.activeBlockIds.length > 0
242
+ const wasActive = initiallyActiveMessages.has(messageId)
243
+
244
+ if (isNowActive && !wasActive) {
245
+ compressedTokens += entry.tokenCount
246
+ newlyCompressedMessageIds.push(messageId)
247
+ }
248
+ }
249
+
250
+ const newlyCompressedToolIds: string[] = []
251
+ for (const toolId of effectiveToolIds) {
252
+ if (!initiallyActiveToolIds.has(toolId)) {
253
+ newlyCompressedToolIds.push(toolId)
254
+ }
255
+ }
256
+
257
+ block.directMessageIds = [...newlyCompressedMessageIds]
258
+ block.directToolIds = [...newlyCompressedToolIds]
259
+
260
+ block.compressedTokens = compressedTokens
261
+
262
+ state.stats.pruneTokenCounter += compressedTokens
263
+ state.stats.totalPruneTokens += state.stats.pruneTokenCounter
264
+ state.stats.pruneTokenCounter = 0
265
+
266
+ return {
267
+ compressedTokens,
268
+ messageIds: selection.messageIds,
269
+ newlyCompressedMessageIds,
270
+ newlyCompressedToolIds,
271
+ }
272
+ }