@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,293 @@
1
+ import type { WithParts } from "../state"
2
+ import { formatBlockRef, formatMessageRef, parseBoundaryId, type IdFormat } from "../message-ids"
3
+
4
+ /**
5
+ * Marker embedded in the hidden summary prompt so the plugin can recognize its
6
+ * own transient (`session.generate`) request and strip everything else from it.
7
+ */
8
+ export const RC_SUMMARY_MARKER = "[[DCP-RC-SUMMARY]]"
9
+
10
+ export interface PriorSummary {
11
+ /** Canonical block reference, e.g. `b2` or `@b2@`. */
12
+ ref: string
13
+ /** Summary body with the block header/footer stripped. */
14
+ body: string
15
+ }
16
+
17
+ export interface SummarySegment {
18
+ /** Canonical selector, e.g. `m0004`, `m0004-m0008`, or `b2-m0011`. */
19
+ selector: string
20
+ messages: WithParts[]
21
+ /** Summaries of active blocks consumed by this selection. */
22
+ priorSummaries?: PriorSummary[]
23
+ }
24
+
25
+ export interface SummarySection {
26
+ selector: string
27
+ summary: string
28
+ }
29
+
30
+ export interface ParsedSummarySections {
31
+ sections: SummarySection[]
32
+ missing: string[]
33
+ duplicates: string[]
34
+ empty: string[]
35
+ }
36
+
37
+ function stringify(value: unknown): string {
38
+ if (typeof value === "string") {
39
+ return value
40
+ }
41
+ try {
42
+ return JSON.stringify(value, null, 2)
43
+ } catch {
44
+ return String(value)
45
+ }
46
+ }
47
+
48
+ function readToolState(part: any): {
49
+ status?: string
50
+ input?: unknown
51
+ output?: unknown
52
+ error?: unknown
53
+ } {
54
+ const state = part?.state
55
+ if (!state || typeof state !== "object") {
56
+ return {}
57
+ }
58
+ return {
59
+ status: typeof state.status === "string" ? state.status : undefined,
60
+ input: state.input,
61
+ output: state.output,
62
+ error: state.error,
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Serialize one conversation message into plain text for the hidden summary
68
+ * request. Binary/file payloads are intentionally omitted.
69
+ */
70
+ export function serializeMessageForSummary(message: WithParts, selector: string): string {
71
+ const role = message.info?.role ?? "unknown"
72
+ const lines: string[] = []
73
+ const parts = Array.isArray(message.parts) ? message.parts : []
74
+
75
+ for (const part of parts as any[]) {
76
+ if (!part || typeof part !== "object") {
77
+ continue
78
+ }
79
+ switch (part.type) {
80
+ case "text":
81
+ if (typeof part.text === "string" && part.text.length > 0) {
82
+ lines.push(part.text)
83
+ }
84
+ break
85
+ case "reasoning":
86
+ if (typeof part.text === "string" && part.text.length > 0) {
87
+ lines.push(`[reasoning] ${part.text}`)
88
+ }
89
+ break
90
+ case "tool": {
91
+ const state = readToolState(part)
92
+ const name = part.tool ?? "tool"
93
+ lines.push(`[tool_call ${name}] ${stringify(state.input)}`)
94
+ if (state.status === "completed") {
95
+ lines.push(`[tool_result ${name}] ${stringify(state.output)}`)
96
+ } else if (state.status === "error") {
97
+ lines.push(`[tool_error ${name}] ${stringify(state.error)}`)
98
+ } else if (state.status) {
99
+ lines.push(`[tool_${state.status} ${name}]`)
100
+ }
101
+ break
102
+ }
103
+ case "file":
104
+ lines.push(`[file ${part.mime ?? ""} ${part.filename ?? part.url ?? ""}]`.trim())
105
+ break
106
+ case "patch":
107
+ lines.push(`[patch] ${stringify(part.hash ?? part.files ?? part)}`)
108
+ break
109
+ case "agent":
110
+ lines.push(`[agent ${part.name ?? ""}]`.trim())
111
+ break
112
+ default:
113
+ break
114
+ }
115
+ }
116
+
117
+ const body = lines.join("\n").trim()
118
+ return `### ${selector} (${role})\n${body.length > 0 ? body : "(empty message)"}`
119
+ }
120
+
121
+ const SUMMARY_RULES = `RULES
122
+ - Reply with one section per selector, in the same order as listed. Nothing else: no introduction, no code fences, no tool calls, no trailing prose.
123
+ - Each section starts with a header line exactly \`SLEEV-SUMMARY <selector>\` using the selector as listed.
124
+ - The summary body continues until the next header or the end of the reply.
125
+ - When a segment lists previous compressed summaries, merge their information into your summary: those blocks are replaced by the section you produce.
126
+ - Preserve every high-signal detail: user intent (quote short user messages exactly), decisions, constraints, file paths, function and symbol names, commands run, key findings, errors, verification results, and any subagent task IDs with their scope and completion state.
127
+ - Drop noise: failed attempts that led nowhere, repeated tool output, exploration that produced no durable result.
128
+ - Be dense and precise. Prefer terse technical wording over narrative.`
129
+
130
+ /**
131
+ * Build the hidden summarization prompt. The selected segments are embedded
132
+ * verbatim so the transient request does not depend on any injected metadata.
133
+ */
134
+ export function buildSummaryPrompt(
135
+ selectors: SummarySegment[],
136
+ idFormat: IdFormat = "xml",
137
+ ): string {
138
+ const selectorList = selectors.map((segment) => segment.selector)
139
+ const example = selectorList[0] ?? (idFormat === "compact" ? "" : "m0004")
140
+ const body = selectors
141
+ .map((segment) => {
142
+ const messageBody = segment.messages
143
+ .map((message) => serializeMessageForSummary(message, segment.selector))
144
+ .join("\n\n")
145
+ const priorBody = (segment.priorSummaries ?? [])
146
+ .map(
147
+ (prior) =>
148
+ `### ${prior.ref} (previous compressed summary)\n${prior.body.trim()}`,
149
+ )
150
+ .join("\n\n")
151
+ return [messageBody, priorBody].filter((part) => part.length > 0).join("\n\n")
152
+ })
153
+ .join("\n\n")
154
+
155
+ return `${RC_SUMMARY_MARKER}
156
+
157
+ You generate compressed-context entries for a coding session. Summarize ONLY the conversation segments listed below. Do not summarize anything else and do not ask questions.
158
+
159
+ SUMMARY FORMAT
160
+ SLEEV-SUMMARY ${example}
161
+ <summary body>
162
+
163
+ ${SUMMARY_RULES.replace("one section per selector", `${selectorList.length} section(s), one per selector`)}
164
+ - The selectors for this request are exactly: ${selectorList.map((selector) => `\`${selector}\``).join(", ")}
165
+ RC-SELECTORS: ${selectorList.join(", ")}
166
+
167
+ SEGMENTS
168
+ ${body}
169
+ `
170
+ }
171
+
172
+ function selectorPattern(): string {
173
+ const boundary = "(?:m[0-9]{4}|@[0-9]+@|b[1-9][0-9]*|@b[1-9][0-9]*@)"
174
+ return `(?:${boundary})(?:-${boundary})?`
175
+ }
176
+
177
+ /**
178
+ * Normalize a selector written in either ID format (`m0004` or ``) into the
179
+ * session's canonical format. Returns null when the selector is malformed.
180
+ */
181
+ export function canonicalizeSelector(selector: string, format: IdFormat = "xml"): string | null {
182
+ const parts = selector.trim().toLowerCase().split("-")
183
+ if (parts.length < 1 || parts.length > 2) {
184
+ return null
185
+ }
186
+ const parse = (part: string) => parseBoundaryId(part, "xml") ?? parseBoundaryId(part, "compact")
187
+ const toRef = (parsed: NonNullable<ReturnType<typeof parse>>): string =>
188
+ parsed.kind === "message"
189
+ ? formatMessageRef(parsed.index, format)
190
+ : formatBlockRef(parsed.blockId, format)
191
+
192
+ const start = parse(parts[0]!)
193
+ const end = parts.length === 2 ? parse(parts[1]!) : start
194
+ if (!start || !end) {
195
+ return null
196
+ }
197
+ if (start.kind === "message" && end.kind === "message" && start.index > end.index) {
198
+ return null
199
+ }
200
+ const startRef = toRef(start)
201
+ const endRef = toRef(end)
202
+ return startRef === endRef ? startRef : `${startRef}-${endRef}`
203
+ }
204
+
205
+ /**
206
+ * Strip the `[Compressed conversation section]` header and trailing block tag
207
+ * from a stored summary so it can be embedded in a new summary prompt.
208
+ */
209
+ export function restoreSummary(summary: string): string {
210
+ const headerMatch = summary.match(
211
+ /^[ \t]*\[Compressed conversation(?: section)?(?: b[0-9]+)?\]/i,
212
+ )
213
+ if (!headerMatch) {
214
+ return summary
215
+ }
216
+
217
+ const body = summary.slice(headerMatch[0].length).trimStart()
218
+ const lines = body.split("\n")
219
+ while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") {
220
+ lines.pop()
221
+ }
222
+ const last = (lines[lines.length - 1] ?? "").trim()
223
+ const isXmlTag = new RegExp("^<dcp-message-id>b[0-9]+</dcp-message-id>$").test(last)
224
+ const isCompactTag = /^@b[1-9][0-9]*@$/.test(last)
225
+ if (isXmlTag || isCompactTag) {
226
+ lines.pop()
227
+ }
228
+ return lines.join("\n").trimEnd()
229
+ }
230
+
231
+ /**
232
+ * Parse `SLEEV-SUMMARY <selector>` sections from a model reply. Returns the
233
+ * accepted sections plus the selectors that are missing, duplicated, or have
234
+ * an empty body so the caller can retry only what is needed. Selectors are
235
+ * accepted in either ID format and normalized to the session format.
236
+ */
237
+ export function parseSummarySections(
238
+ text: string,
239
+ expected: string[],
240
+ idFormat: IdFormat = "xml",
241
+ ): ParsedSummarySections {
242
+ const header = new RegExp(`^SLEEV-SUMMARY (${selectorPattern()})[ \t]*$`, "gmi")
243
+ const matches: Array<{ selector: string; start: number; end: number }> = []
244
+ let match: RegExpExecArray | null
245
+
246
+ while ((match = header.exec(text)) !== null) {
247
+ const selector = canonicalizeSelector((match[1] ?? "").trim(), idFormat)
248
+ if (!selector) {
249
+ continue
250
+ }
251
+ matches.push({
252
+ selector,
253
+ start: match.index,
254
+ end: match.index + match[0].length,
255
+ })
256
+ }
257
+
258
+ const normalizedExpected = expected
259
+ .map((selector) => canonicalizeSelector(selector, idFormat))
260
+ .filter((selector): selector is string => Boolean(selector))
261
+ const expectedSet = new Set(normalizedExpected)
262
+ const sections: SummarySection[] = []
263
+ const seen = new Set<string>()
264
+ const duplicates: string[] = []
265
+ const empty: string[] = []
266
+
267
+ for (let index = 0; index < matches.length; index++) {
268
+ const current = matches[index]!
269
+ if (!expectedSet.has(current.selector)) {
270
+ continue
271
+ }
272
+ if (seen.has(current.selector)) {
273
+ if (!duplicates.includes(current.selector)) {
274
+ duplicates.push(current.selector)
275
+ }
276
+ continue
277
+ }
278
+ seen.add(current.selector)
279
+
280
+ const next = matches[index + 1]
281
+ const body = text.slice(current.end, next ? next.start : text.length).trim()
282
+ if (body.length === 0) {
283
+ empty.push(current.selector)
284
+ continue
285
+ }
286
+ sections.push({ selector: current.selector, summary: body })
287
+ }
288
+
289
+ const missing = normalizedExpected.filter(
290
+ (selector) => !seen.has(selector) && !empty.includes(selector),
291
+ )
292
+ return { sections, missing, duplicates, empty }
293
+ }
@@ -0,0 +1,77 @@
1
+ import type { SessionState } from "../state/types"
2
+ import { attachCompressionDuration } from "./state"
3
+
4
+ export interface PendingCompressionDuration {
5
+ messageId: string
6
+ callId: string
7
+ durationMs: number
8
+ }
9
+
10
+ export interface CompressionTimingState {
11
+ startsByCallId: Map<string, number>
12
+ pendingByCallId: Map<string, PendingCompressionDuration>
13
+ }
14
+
15
+ export function buildCompressionTimingKey(messageId: string, callId: string): string {
16
+ return `${messageId}:${callId}`
17
+ }
18
+
19
+ export function consumeCompressionStart(
20
+ state: SessionState,
21
+ messageId: string,
22
+ callId: string,
23
+ ): number | undefined {
24
+ const key = buildCompressionTimingKey(messageId, callId)
25
+ const start = state.compressionTiming.startsByCallId.get(key)
26
+ state.compressionTiming.startsByCallId.delete(key)
27
+ return start
28
+ }
29
+
30
+ export function resolveCompressionDuration(
31
+ startedAt: number | undefined,
32
+ eventTime: number | undefined,
33
+ partTime: { start?: unknown; end?: unknown } | undefined,
34
+ ): number | undefined {
35
+ const runningAt =
36
+ typeof partTime?.start === "number" && Number.isFinite(partTime.start)
37
+ ? partTime.start
38
+ : eventTime
39
+ const pendingToRunningMs =
40
+ typeof startedAt === "number" && typeof runningAt === "number"
41
+ ? Math.max(0, runningAt - startedAt)
42
+ : undefined
43
+
44
+ const toolStart = partTime?.start
45
+ const toolEnd = partTime?.end
46
+ const runtimeMs =
47
+ typeof toolStart === "number" &&
48
+ Number.isFinite(toolStart) &&
49
+ typeof toolEnd === "number" &&
50
+ Number.isFinite(toolEnd)
51
+ ? Math.max(0, toolEnd - toolStart)
52
+ : undefined
53
+
54
+ return typeof pendingToRunningMs === "number" ? pendingToRunningMs : runtimeMs
55
+ }
56
+
57
+ export function applyPendingCompressionDurations(state: SessionState): number {
58
+ if (state.compressionTiming.pendingByCallId.size === 0) {
59
+ return 0
60
+ }
61
+
62
+ let updates = 0
63
+ for (const [key, entry] of state.compressionTiming.pendingByCallId) {
64
+ const applied = attachCompressionDuration(
65
+ state.prune.messages,
66
+ entry.messageId,
67
+ entry.callId,
68
+ entry.durationMs,
69
+ )
70
+ if (applied > 0) {
71
+ updates += applied
72
+ state.compressionTiming.pendingByCallId.delete(key)
73
+ }
74
+ }
75
+
76
+ return updates
77
+ }
@@ -0,0 +1,57 @@
1
+ import type { PluginConfig } from "../config"
2
+ import type { Logger } from "../logger"
3
+ import type { PromptStore } from "../prompts/store"
4
+ import type { CompressionBlock, CompressionMode, SessionState, WithParts } from "../state"
5
+
6
+ export interface ToolContext {
7
+ client: any
8
+ state: SessionState
9
+ logger: Logger
10
+ config: PluginConfig
11
+ prompts: PromptStore
12
+ /** Hidden transient model call (OpenCode V2 `session.generate`). */
13
+ generate: (input: { sessionID: string; prompt: string }) => Promise<string>
14
+ }
15
+
16
+ export interface BoundaryReference {
17
+ kind: "message" | "compressed-block"
18
+ rawIndex: number
19
+ messageId?: string
20
+ blockId?: number
21
+ anchorMessageId?: string
22
+ }
23
+
24
+ export interface SearchContext {
25
+ rawMessages: WithParts[]
26
+ rawMessagesById: Map<string, WithParts>
27
+ rawIndexById: Map<string, number>
28
+ summaryByBlockId: Map<number, CompressionBlock>
29
+ }
30
+
31
+ export interface SelectionResolution {
32
+ startReference: BoundaryReference
33
+ endReference: BoundaryReference
34
+ messageIds: string[]
35
+ messageTokenById: Map<string, number>
36
+ toolIds: string[]
37
+ requiredBlockIds: number[]
38
+ }
39
+
40
+ export interface AppliedCompressionResult {
41
+ compressedTokens: number
42
+ messageIds: string[]
43
+ newlyCompressedMessageIds: string[]
44
+ newlyCompressedToolIds: string[]
45
+ }
46
+
47
+ export interface CompressionStateInput {
48
+ topic: string
49
+ batchTopic: string
50
+ startId: string
51
+ endId: string
52
+ mode: CompressionMode
53
+ runId: number
54
+ compressMessageId: string
55
+ compressCallId?: string
56
+ summaryTokens: number
57
+ }
@@ -0,0 +1,9 @@
1
+ import type { PluginConfig } from "./config"
2
+ import type { SessionState } from "./state"
3
+
4
+ export const compressPermission = (
5
+ state: SessionState,
6
+ config: PluginConfig,
7
+ ): "ask" | "allow" | "deny" => {
8
+ return state.compressPermission ?? config.compress.permission
9
+ }