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

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 (215) 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 +8129 -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 +27 -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 +68 -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 +64 -0
  42. package/dist/lib/compress/types.d.ts.map +1 -0
  43. package/dist/lib/compress/usage.d.ts +45 -0
  44. package/dist/lib/compress/usage.d.ts.map +1 -0
  45. package/dist/lib/compress-permission.d.ts +4 -0
  46. package/dist/lib/compress-permission.d.ts.map +1 -0
  47. package/dist/lib/config.d.ts +83 -0
  48. package/dist/lib/config.d.ts.map +1 -0
  49. package/dist/lib/hooks.d.ts +22 -0
  50. package/dist/lib/hooks.d.ts.map +1 -0
  51. package/dist/lib/logger.d.ts +31 -0
  52. package/dist/lib/logger.d.ts.map +1 -0
  53. package/dist/lib/message-ids.d.ts +20 -0
  54. package/dist/lib/message-ids.d.ts.map +1 -0
  55. package/dist/lib/messages/index.d.ts +8 -0
  56. package/dist/lib/messages/index.d.ts.map +1 -0
  57. package/dist/lib/messages/inject/inject.d.ts +7 -0
  58. package/dist/lib/messages/inject/inject.d.ts.map +1 -0
  59. package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
  60. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
  61. package/dist/lib/messages/inject/utils.d.ts +23 -0
  62. package/dist/lib/messages/inject/utils.d.ts.map +1 -0
  63. package/dist/lib/messages/prune.d.ts +4 -0
  64. package/dist/lib/messages/prune.d.ts.map +1 -0
  65. package/dist/lib/messages/query.d.ts +7 -0
  66. package/dist/lib/messages/query.d.ts.map +1 -0
  67. package/dist/lib/messages/reasoning-strip.d.ts +8 -0
  68. package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
  69. package/dist/lib/messages/shape.d.ts +5 -0
  70. package/dist/lib/messages/shape.d.ts.map +1 -0
  71. package/dist/lib/messages/sync.d.ts +4 -0
  72. package/dist/lib/messages/sync.d.ts.map +1 -0
  73. package/dist/lib/messages/utils.d.ts +27 -0
  74. package/dist/lib/messages/utils.d.ts.map +1 -0
  75. package/dist/lib/prompts/compress-rc.d.ts +4 -0
  76. package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
  77. package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
  78. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
  79. package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
  80. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  81. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  82. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  83. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  84. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  85. package/dist/lib/prompts/index.d.ts +4 -0
  86. package/dist/lib/prompts/index.d.ts.map +1 -0
  87. package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
  88. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
  89. package/dist/lib/prompts/store.d.ts +26 -0
  90. package/dist/lib/prompts/store.d.ts.map +1 -0
  91. package/dist/lib/prompts/system.d.ts +4 -0
  92. package/dist/lib/prompts/system.d.ts.map +1 -0
  93. package/dist/lib/prompts/turn-nudge.d.ts +2 -0
  94. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
  95. package/dist/lib/protected-patterns.d.ts +6 -0
  96. package/dist/lib/protected-patterns.d.ts.map +1 -0
  97. package/dist/lib/state/index.d.ts +5 -0
  98. package/dist/lib/state/index.d.ts.map +1 -0
  99. package/dist/lib/state/persistence.d.ts +47 -0
  100. package/dist/lib/state/persistence.d.ts.map +1 -0
  101. package/dist/lib/state/state.d.ts +9 -0
  102. package/dist/lib/state/state.d.ts.map +1 -0
  103. package/dist/lib/state/tool-cache.d.ts +13 -0
  104. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  105. package/dist/lib/state/types.d.ts +105 -0
  106. package/dist/lib/state/types.d.ts.map +1 -0
  107. package/dist/lib/state/utils.d.ts +22 -0
  108. package/dist/lib/state/utils.d.ts.map +1 -0
  109. package/dist/lib/strategies/deduplication.d.ts +10 -0
  110. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  111. package/dist/lib/strategies/index.d.ts +3 -0
  112. package/dist/lib/strategies/index.d.ts.map +1 -0
  113. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  114. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  115. package/dist/lib/subagents/subagent-results.d.ts +5 -0
  116. package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
  117. package/dist/lib/token-utils.d.ts +19 -0
  118. package/dist/lib/token-utils.d.ts.map +1 -0
  119. package/dist/lib/tui/dialogs.d.ts +29 -0
  120. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  121. package/dist/lib/tui/format.d.ts +4 -0
  122. package/dist/lib/tui/format.d.ts.map +1 -0
  123. package/dist/lib/tui/types.d.ts +18 -0
  124. package/dist/lib/tui/types.d.ts.map +1 -0
  125. package/dist/lib/tui/ui.d.ts +48 -0
  126. package/dist/lib/tui/ui.d.ts.map +1 -0
  127. package/dist/lib/ui/notification.d.ts +17 -0
  128. package/dist/lib/ui/notification.d.ts.map +1 -0
  129. package/dist/lib/ui/utils.d.ts +10 -0
  130. package/dist/lib/ui/utils.d.ts.map +1 -0
  131. package/dist/lib/v2/index.d.ts +5 -0
  132. package/dist/lib/v2/index.d.ts.map +1 -0
  133. package/dist/lib/v2/messages.d.ts +13 -0
  134. package/dist/lib/v2/messages.d.ts.map +1 -0
  135. package/dist/lib/v2/rpc.d.ts +75 -0
  136. package/dist/lib/v2/rpc.d.ts.map +1 -0
  137. package/dist/lib/v2/theme.d.ts +55 -0
  138. package/dist/lib/v2/theme.d.ts.map +1 -0
  139. package/dist/lib/v2/tui.d.ts +4 -0
  140. package/dist/lib/v2/tui.d.ts.map +1 -0
  141. package/dist/lib/v2/usage.d.ts +44 -0
  142. package/dist/lib/v2/usage.d.ts.map +1 -0
  143. package/dist/tui.d.ts +7 -0
  144. package/dist/tui.d.ts.map +1 -0
  145. package/lib/commands/compression-targets.ts +54 -0
  146. package/lib/commands/context.ts +305 -0
  147. package/lib/commands/decompress.ts +273 -0
  148. package/lib/commands/help.ts +76 -0
  149. package/lib/commands/index.ts +11 -0
  150. package/lib/commands/manual.ts +126 -0
  151. package/lib/commands/recompress.ts +222 -0
  152. package/lib/commands/stats.ts +180 -0
  153. package/lib/commands/sweep.ts +266 -0
  154. package/lib/compress/index.ts +2 -0
  155. package/lib/compress/pipeline.ts +116 -0
  156. package/lib/compress/protected-content.ts +163 -0
  157. package/lib/compress/rc.ts +494 -0
  158. package/lib/compress/search.ts +277 -0
  159. package/lib/compress/state.ts +272 -0
  160. package/lib/compress/summary.ts +320 -0
  161. package/lib/compress/timing.ts +77 -0
  162. package/lib/compress/types.ts +70 -0
  163. package/lib/compress/usage.ts +168 -0
  164. package/lib/compress-permission.ts +9 -0
  165. package/lib/config.ts +995 -0
  166. package/lib/hooks.ts +205 -0
  167. package/lib/logger.ts +226 -0
  168. package/lib/message-ids.ts +181 -0
  169. package/lib/messages/index.ts +7 -0
  170. package/lib/messages/inject/inject.ts +215 -0
  171. package/lib/messages/inject/subagent-results.ts +84 -0
  172. package/lib/messages/inject/utils.ts +304 -0
  173. package/lib/messages/prune.ts +238 -0
  174. package/lib/messages/query.ts +71 -0
  175. package/lib/messages/reasoning-strip.ts +40 -0
  176. package/lib/messages/shape.ts +50 -0
  177. package/lib/messages/sync.ts +124 -0
  178. package/lib/messages/utils.ts +190 -0
  179. package/lib/prompts/compress-rc.ts +25 -0
  180. package/lib/prompts/context-limit-nudge.ts +18 -0
  181. package/lib/prompts/extensions/nudge.ts +34 -0
  182. package/lib/prompts/extensions/system.ts +32 -0
  183. package/lib/prompts/extensions/tool.ts +21 -0
  184. package/lib/prompts/index.ts +29 -0
  185. package/lib/prompts/iteration-nudge.ts +6 -0
  186. package/lib/prompts/store.ts +452 -0
  187. package/lib/prompts/system.ts +41 -0
  188. package/lib/prompts/turn-nudge.ts +10 -0
  189. package/lib/protected-patterns.ts +159 -0
  190. package/lib/state/index.ts +4 -0
  191. package/lib/state/persistence.ts +321 -0
  192. package/lib/state/state.ts +214 -0
  193. package/lib/state/tool-cache.ts +99 -0
  194. package/lib/state/types.ts +117 -0
  195. package/lib/state/utils.ts +345 -0
  196. package/lib/strategies/deduplication.ts +125 -0
  197. package/lib/strategies/index.ts +2 -0
  198. package/lib/strategies/purge-errors.ts +86 -0
  199. package/lib/subagents/subagent-results.ts +75 -0
  200. package/lib/token-utils.ts +164 -0
  201. package/lib/tui/dialogs.tsx +261 -0
  202. package/lib/tui/format.ts +25 -0
  203. package/lib/tui/types.ts +25 -0
  204. package/lib/tui/ui.tsx +245 -0
  205. package/lib/ui/notification.ts +347 -0
  206. package/lib/ui/utils.ts +304 -0
  207. package/lib/v2/index.ts +448 -0
  208. package/lib/v2/messages.ts +252 -0
  209. package/lib/v2/rpc.ts +60 -0
  210. package/lib/v2/theme.ts +67 -0
  211. package/lib/v2/tui.tsx +115 -0
  212. package/lib/v2/usage.ts +423 -0
  213. package/package.json +91 -5
  214. package/server.js +1 -0
  215. package/tui.tsx +7 -0
@@ -0,0 +1,494 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import type { ToolContext, SelectionResolution } from "./types"
3
+ import type { WithParts } from "../state"
4
+ import { refreshManualMode } from "../state"
5
+ import { formatBlockRef, formatMessageRef, parseBoundaryId, type IdFormat } from "../message-ids"
6
+ import {
7
+ buildSearchContext,
8
+ fetchSessionMessages,
9
+ resolveAnchorMessageId,
10
+ resolveBoundaryIds,
11
+ resolveSelection,
12
+ } from "./search"
13
+ import {
14
+ allocateBlockId,
15
+ allocateRunId,
16
+ applyCompressionState,
17
+ wrapCompressedSummary,
18
+ } from "./state"
19
+ import { finalizeSession, prepareSession, type NotificationEntry } from "./pipeline"
20
+ import { countTokens } from "../token-utils"
21
+ import { saveSessionState } from "../state/persistence"
22
+ import { rcFormat } from "../prompts/extensions/tool"
23
+ import { appendProtectedPromptInfo, appendProtectedTools } from "./protected-content"
24
+ import {
25
+ addCompressionUsage,
26
+ addCompressionUsageTotals,
27
+ emptyCompressionUsage,
28
+ estimatedCompressionUsage,
29
+ type CompressionUsageTotals,
30
+ } from "./usage"
31
+ import {
32
+ buildSummaryPrompt,
33
+ parseSummarySections,
34
+ restoreSummary,
35
+ type PriorSummary,
36
+ type SummarySection,
37
+ type SummarySegment,
38
+ } from "./summary"
39
+
40
+ export interface RcCompressToolArgs {
41
+ ids: string[]
42
+ }
43
+
44
+ let summaryCallCounter = 0
45
+
46
+ /** Unique per hidden summary request so provider usage can be attributed. */
47
+ function nextSummaryCallId(): string {
48
+ summaryCallCounter += 1
49
+ return `c${summaryCallCounter.toString(36)}-${Date.now().toString(36)}-${Math.random()
50
+ .toString(36)
51
+ .slice(2, 8)}`
52
+ }
53
+
54
+ /**
55
+ * Compress selections with a hidden, synchronous summary request.
56
+ *
57
+ * The model only selects message IDs (`ids`); summaries are generated by the
58
+ * plugin in a transient `session.generate` call during this tool call and are
59
+ * applied before the model's next step. Nothing except the `compress` tool
60
+ * call itself is user-visible.
61
+ */
62
+
63
+ function validateArgs(args: RcCompressToolArgs): void {
64
+ if (!args || typeof args !== "object") {
65
+ throw new Error("compress requires an object argument with an `ids` array")
66
+ }
67
+ if (!Array.isArray(args.ids)) {
68
+ throw new Error("`ids` must be an array of message IDs or inclusive ranges")
69
+ }
70
+ if (args.ids.length === 0) {
71
+ throw new Error("`ids` must not be empty")
72
+ }
73
+ }
74
+
75
+ interface ParsedSelector {
76
+ selector: string
77
+ startRef: string
78
+ endRef: string
79
+ }
80
+
81
+ export function parseSelector(selector: string, format: IdFormat): ParsedSelector {
82
+ const raw = selector.trim().toLowerCase()
83
+ const parts = raw.split("-").map((part) => part.trim())
84
+ if (parts.length === 1 || parts.length === 2) {
85
+ const parseAny = (part: string) =>
86
+ parseBoundaryId(part, format) ??
87
+ parseBoundaryId(part, format === "compact" ? "xml" : "compact")
88
+ const toRef = (parsed: NonNullable<ReturnType<typeof parseAny>>): string =>
89
+ parsed.kind === "message"
90
+ ? formatMessageRef(parsed.index, format)
91
+ : formatBlockRef(parsed.blockId, format)
92
+
93
+ const start = parseAny(parts[0]!)
94
+ const end = parts.length === 2 ? parseAny(parts[1]!) : start
95
+ if (start && end) {
96
+ if (start.kind === "message" && end.kind === "message" && start.index > end.index) {
97
+ throw new Error(`Invalid selector ${selector}: start comes after end`)
98
+ }
99
+ const startRef = toRef(start)
100
+ const endRef = toRef(end)
101
+ return {
102
+ selector: startRef === endRef ? startRef : `${startRef}-${endRef}`,
103
+ startRef,
104
+ endRef,
105
+ }
106
+ }
107
+ }
108
+ throw new Error(
109
+ `Invalid selector ${JSON.stringify(selector)}. Use an injected message ID (e.g. ${
110
+ format === "compact" ? "" : "m0004"
111
+ }), a block ID (e.g. ${
112
+ format === "compact" ? "" : "b2"
113
+ }), or an inclusive range of either.`,
114
+ )
115
+ }
116
+
117
+ interface RcPlan {
118
+ selector: ParsedSelector
119
+ messageIds: string[]
120
+ messages: WithParts[]
121
+ selection: SelectionResolution
122
+ anchorMessageId: string
123
+ priorSummaries: PriorSummary[]
124
+ }
125
+
126
+ function planSelections(
127
+ ctx: ToolContext,
128
+ rawMessages: WithParts[],
129
+ selectors: ParsedSelector[],
130
+ ): RcPlan[] {
131
+ const context = buildSearchContext(ctx.state, rawMessages)
132
+
133
+ return selectors.map((selector) => {
134
+ const { startReference, endReference } = resolveBoundaryIds(
135
+ context,
136
+ ctx.state,
137
+ ctx.config,
138
+ selector.startRef,
139
+ selector.endRef,
140
+ )
141
+ const selection = resolveSelection(context, ctx.config, startReference, endReference)
142
+ const messages = selection.messageIds
143
+ .map((messageId) => context.rawMessagesById.get(messageId))
144
+ .filter((message): message is WithParts => Boolean(message))
145
+ const anchorMessageId = resolveAnchorMessageId(startReference)
146
+
147
+ const priorSummaries: PriorSummary[] = []
148
+ for (const blockId of selection.requiredBlockIds) {
149
+ const block = ctx.state.prune.messages.blocksById.get(blockId)
150
+ if (!block) {
151
+ continue
152
+ }
153
+ const body = restoreSummary(block.summary)
154
+ if (body.length > 0) {
155
+ priorSummaries.push({
156
+ ref: formatBlockRef(blockId, ctx.state.idFormat),
157
+ body,
158
+ })
159
+ }
160
+ }
161
+
162
+ return {
163
+ selector,
164
+ messageIds: selection.messageIds,
165
+ messages,
166
+ selection,
167
+ anchorMessageId,
168
+ priorSummaries,
169
+ }
170
+ })
171
+ }
172
+
173
+ function validateNonOverlapping(plans: RcPlan[]): void {
174
+ const sorted = [...plans].sort(
175
+ (left, right) =>
176
+ left.selection.startReference.rawIndex - right.selection.startReference.rawIndex ||
177
+ left.selection.endReference.rawIndex - right.selection.endReference.rawIndex,
178
+ )
179
+
180
+ for (let index = 1; index < sorted.length; index++) {
181
+ const previous = sorted[index - 1]
182
+ const current = sorted[index]
183
+ if (!previous || !current) {
184
+ continue
185
+ }
186
+ if (current.selection.startReference.rawIndex <= previous.selection.endReference.rawIndex) {
187
+ throw new Error(
188
+ `Selectors ${previous.selector.selector} and ${current.selector.selector} overlap. Overlapping selections cannot be compressed in one call.`,
189
+ )
190
+ }
191
+ }
192
+ }
193
+
194
+ function assertBlockCoverage(
195
+ ctx: ToolContext,
196
+ selector: string,
197
+ selection: SelectionResolution,
198
+ ): void {
199
+ const required = new Set(selection.requiredBlockIds)
200
+ for (const messageId of selection.messageIds) {
201
+ const entry = ctx.state.prune.messages.byMessageId.get(messageId)
202
+ if (!entry || entry.activeBlockIds.length === 0) {
203
+ continue
204
+ }
205
+ for (const blockId of entry.activeBlockIds) {
206
+ if (required.has(blockId)) {
207
+ continue
208
+ }
209
+ const ref = formatBlockRef(blockId, ctx.state.idFormat)
210
+ throw new Error(
211
+ `Selector ${selector} only partially covers compressed block ${ref}. Include ${ref} as a boundary so its summary can be merged, or exclude its messages.`,
212
+ )
213
+ }
214
+ }
215
+ }
216
+
217
+ export function createRcCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
218
+ ctx.prompts.reload()
219
+ const runtimePrompts = ctx.prompts.getRuntimePrompts()
220
+ return tool({
221
+ description: runtimePrompts.compressRc + rcFormat(ctx.state.idFormat),
222
+ args: {
223
+ ids: tool.schema
224
+ .array(tool.schema.string())
225
+ .describe(
226
+ "The injected message or block IDs, or inclusive ranges, to compress. Each item becomes one summary.",
227
+ ),
228
+ },
229
+ async execute(args, toolCtx) {
230
+ const input = (args ?? {}) as RcCompressToolArgs
231
+ validateArgs(input)
232
+
233
+ const sessionID =
234
+ typeof (toolCtx as any)?.sessionID === "string" ? (toolCtx as any).sessionID : ""
235
+ if (!sessionID) {
236
+ throw new Error("compress could not determine the session")
237
+ }
238
+ const callId =
239
+ typeof (toolCtx as any)?.callID === "string" ? (toolCtx as any).callID : undefined
240
+ const toolMessageID = String((toolCtx as any)?.messageID ?? "")
241
+
242
+ try {
243
+ const { rawMessages } = await prepareSession(
244
+ ctx,
245
+ toolCtx as any,
246
+ `Compress: ${input.ids.join(", ")}`,
247
+ )
248
+
249
+ const selectors = input.ids.map((selector) =>
250
+ parseSelector(selector, ctx.state.idFormat),
251
+ )
252
+ const plans = planSelections(ctx, rawMessages, selectors)
253
+ validateNonOverlapping(plans)
254
+ for (const plan of plans) {
255
+ assertBlockCoverage(ctx, plan.selector.selector, plan.selection)
256
+ }
257
+
258
+ const segments: SummarySegment[] = plans.map((plan) => ({
259
+ selector: plan.selector.selector,
260
+ messages: plan.messages,
261
+ priorSummaries: plan.priorSummaries,
262
+ }))
263
+
264
+ const { text, usage } = await generateSummaries(ctx, sessionID, segments)
265
+ ctx.state.stats.compressionUsage = addCompressionUsageTotals(
266
+ ctx.state.stats.compressionUsage,
267
+ usage,
268
+ )
269
+ await saveSessionState(ctx.state, ctx.logger)
270
+ const accepted = await resolveAcceptedSections(
271
+ ctx,
272
+ plans.map((plan) => plan.selector.selector),
273
+ text,
274
+ )
275
+ if (accepted.length === 0) {
276
+ throw new Error("summary reply contained no usable sections")
277
+ }
278
+
279
+ const { entries, applied, missing } = await applySummaries(
280
+ ctx,
281
+ sessionID,
282
+ toolMessageID,
283
+ callId,
284
+ plans,
285
+ accepted,
286
+ )
287
+ if (entries.length === 0) {
288
+ throw new Error("summary reply could not be applied to the session")
289
+ }
290
+
291
+ await finalizeSession(
292
+ ctx,
293
+ toolCtx as any,
294
+ rawMessages,
295
+ entries,
296
+ plans.map((plan) => plan.selector.selector).join(", "),
297
+ )
298
+
299
+ if (missing.length === 0) {
300
+ return `Compressed: ${applied.join(", ")}.`
301
+ }
302
+ return `Compressed: ${applied.join(", ")}. Not compressed: ${missing.join(", ")}.`
303
+ } catch (error) {
304
+ await resetManualMode(ctx, sessionID)
305
+ throw error
306
+ }
307
+ },
308
+ })
309
+ }
310
+
311
+ async function generateSummaries(
312
+ ctx: ToolContext,
313
+ sessionID: string,
314
+ segments: SummarySegment[],
315
+ ): Promise<{ text: string; usage: CompressionUsageTotals }> {
316
+ let usage = emptyCompressionUsage()
317
+
318
+ const summarize = async (promptSegments: SummarySegment[]): Promise<string> => {
319
+ const callId = nextSummaryCallId()
320
+ const prompt = buildSummaryPrompt(promptSegments, ctx.state.idFormat, callId)
321
+ const result = await ctx.generate({ sessionID, prompt, callId })
322
+ const text = result.text ?? ""
323
+ // Provider usage is authoritative; fall back to a local estimate when
324
+ // the plugin could not observe the underlying model call.
325
+ usage = addCompressionUsage(
326
+ usage,
327
+ result.usage ?? estimatedCompressionUsage(countTokens(prompt), countTokens(text)),
328
+ )
329
+ return text
330
+ }
331
+
332
+ let text = await summarize(segments)
333
+
334
+ // One retry for sections the model failed to emit.
335
+ for (let retry = 0; retry < 2; retry++) {
336
+ const parsed = parseSummarySections(
337
+ text,
338
+ segments.map((segment) => segment.selector),
339
+ ctx.state.idFormat,
340
+ )
341
+ const retrySelectors = [...parsed.missing, ...parsed.empty]
342
+ if (retrySelectors.length === 0) {
343
+ break
344
+ }
345
+ const retrySegments = segments.filter((segment) =>
346
+ retrySelectors.includes(segment.selector.toLowerCase()),
347
+ )
348
+ if (retrySegments.length === 0) {
349
+ break
350
+ }
351
+ try {
352
+ text += "\n\n" + (await summarize(retrySegments))
353
+ } catch {
354
+ break
355
+ }
356
+ }
357
+ return { text, usage }
358
+ }
359
+
360
+ async function resolveAcceptedSections(
361
+ ctx: ToolContext,
362
+ selectors: string[],
363
+ text: string,
364
+ ): Promise<SummarySection[]> {
365
+ const parsed = parseSummarySections(text, selectors, ctx.state.idFormat)
366
+ ctx.logger.info("rc summary reply parsed", {
367
+ replyChars: text.length,
368
+ accepted: parsed.sections.map((section) => section.selector).join(","),
369
+ missing: parsed.missing.join(","),
370
+ empty: parsed.empty.join(","),
371
+ })
372
+ if (parsed.missing.length > 0 || parsed.empty.length > 0) {
373
+ ctx.logger.warn("rc compression reply was incomplete", {
374
+ missing: parsed.missing.join(","),
375
+ empty: parsed.empty.join(","),
376
+ })
377
+ }
378
+ return parsed.sections
379
+ }
380
+
381
+ async function applySummaries(
382
+ ctx: ToolContext,
383
+ sessionID: string,
384
+ toolMessageID: string,
385
+ callId: string | undefined,
386
+ plans: RcPlan[],
387
+ sections: SummarySection[],
388
+ ): Promise<{ entries: NotificationEntry[]; applied: string[]; missing: string[] }> {
389
+ // Re-read the session so we only apply summaries whose source messages are
390
+ // still present (a compaction or revert may have happened meanwhile).
391
+ const rawMessages = await fetchSessionMessages(ctx.client, sessionID)
392
+ const context = buildSearchContext(ctx.state, rawMessages)
393
+ const planBySelector = new Map(plans.map((plan) => [plan.selector.selector, plan]))
394
+
395
+ const runId = allocateRunId(ctx.state)
396
+ const batchTopic = plans.map((plan) => plan.selector.selector).join(", ")
397
+ const entries: NotificationEntry[] = []
398
+ const applied: string[] = []
399
+ const appliedSet = new Set<string>()
400
+
401
+ for (const section of sections) {
402
+ const plan = planBySelector.get(section.selector)
403
+ if (!plan) {
404
+ continue
405
+ }
406
+
407
+ try {
408
+ const { startReference, endReference } = resolveBoundaryIds(
409
+ context,
410
+ ctx.state,
411
+ ctx.config,
412
+ plan.selector.startRef,
413
+ plan.selector.endRef,
414
+ )
415
+ const selection = resolveSelection(context, ctx.config, startReference, endReference)
416
+ if (selection.messageIds.length === 0) {
417
+ continue
418
+ }
419
+ assertBlockCoverage(ctx, section.selector, selection)
420
+
421
+ const anchorMessageId = resolveAnchorMessageId(startReference)
422
+ const blockId = allocateBlockId(ctx.state)
423
+
424
+ const withPromptInfo = appendProtectedPromptInfo(
425
+ section.summary,
426
+ selection,
427
+ context,
428
+ ctx.state,
429
+ ctx.config.compress.protectTags,
430
+ )
431
+ const finalSummary = await appendProtectedTools(
432
+ ctx.client,
433
+ ctx.state,
434
+ ctx.config.experimental.allowSubAgents,
435
+ withPromptInfo,
436
+ selection,
437
+ context,
438
+ ctx.config.compress.protectedTools,
439
+ ctx.config.protectedFilePatterns,
440
+ )
441
+
442
+ const storedSummary = wrapCompressedSummary(blockId, finalSummary, ctx.state.idFormat)
443
+ const summaryTokens = countTokens(storedSummary)
444
+
445
+ applyCompressionState(
446
+ ctx.state,
447
+ {
448
+ topic: section.selector,
449
+ batchTopic,
450
+ startId: plan.selector.startRef,
451
+ endId: plan.selector.endRef,
452
+ mode: "rc",
453
+ runId,
454
+ compressMessageId: toolMessageID,
455
+ compressCallId: callId,
456
+ summaryTokens,
457
+ },
458
+ selection,
459
+ anchorMessageId,
460
+ blockId,
461
+ storedSummary,
462
+ selection.requiredBlockIds,
463
+ )
464
+
465
+ entries.push({
466
+ blockId,
467
+ runId,
468
+ summary: finalSummary,
469
+ summaryTokens,
470
+ })
471
+ applied.push(section.selector)
472
+ appliedSet.add(section.selector)
473
+ } catch (error) {
474
+ ctx.logger.warn("rc compression could not apply a summary", {
475
+ selector: section.selector,
476
+ error: error instanceof Error ? error.message : String(error),
477
+ })
478
+ }
479
+ }
480
+
481
+ const missing = plans
482
+ .map((plan) => plan.selector.selector)
483
+ .filter((selector) => !appliedSet.has(selector))
484
+ return { entries, applied, missing }
485
+ }
486
+
487
+ async function resetManualMode(ctx: ToolContext, sessionID: string): Promise<void> {
488
+ if (ctx.state.manualMode !== "compress-pending") {
489
+ return
490
+ }
491
+ ctx.state.manualMode = false
492
+ await refreshManualMode(ctx.state, sessionID, ctx.logger, ctx.config.manualMode.enabled)
493
+ await saveSessionState(ctx.state, ctx.logger)
494
+ }