@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,266 @@
1
+ /**
2
+ * DCP Sweep command handler.
3
+ * Prunes tool outputs since the last user message, or the last N tools.
4
+ *
5
+ * Usage:
6
+ * /dcp sweep - Prune all tools since the previous user message
7
+ * /dcp sweep 10 - Prune the last 10 tools
8
+ */
9
+
10
+ import type { Logger } from "../logger"
11
+ import type { SessionState, WithParts, ToolParameterEntry } from "../state"
12
+ import type { PluginConfig } from "../config"
13
+ import { sendIgnoredMessage } from "../ui/notification"
14
+ import { formatPrunedItemsList } from "../ui/utils"
15
+ import { getCurrentParams, getTotalToolTokens } from "../token-utils"
16
+ import { isIgnoredUserMessage } from "../messages/query"
17
+ import { buildToolIdList } from "../messages/utils"
18
+ import { saveSessionState } from "../state/persistence"
19
+ import { isMessageCompacted } from "../state/utils"
20
+ import { isToolProtected } from "../protected-patterns"
21
+ import { syncToolCache } from "../state/tool-cache"
22
+
23
+ export interface SweepCommandContext {
24
+ client: any
25
+ state: SessionState
26
+ config: PluginConfig
27
+ logger: Logger
28
+ sessionId: string
29
+ messages: WithParts[]
30
+ args: string[]
31
+ workingDirectory: string
32
+ }
33
+
34
+ function findLastUserMessageIndex(messages: WithParts[]): number {
35
+ for (let i = messages.length - 1; i >= 0; i--) {
36
+ const msg = messages[i]
37
+ if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
38
+ return i
39
+ }
40
+ }
41
+
42
+ return -1
43
+ }
44
+
45
+ function collectToolIdsAfterIndex(
46
+ state: SessionState,
47
+ messages: WithParts[],
48
+ afterIndex: number,
49
+ ): string[] {
50
+ const toolIds: string[] = []
51
+
52
+ for (let i = afterIndex + 1; i < messages.length; i++) {
53
+ const msg = messages[i]
54
+ if (isMessageCompacted(state, msg)) {
55
+ continue
56
+ }
57
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
58
+ if (parts.length > 0) {
59
+ for (const part of parts) {
60
+ if (part.type === "tool" && part.callID && part.tool) {
61
+ toolIds.push(part.callID)
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ return toolIds
68
+ }
69
+
70
+ function formatNoUserMessage(): string {
71
+ const lines: string[] = []
72
+
73
+ lines.push("╭───────────────────────────────────────────────────────────╮")
74
+ lines.push("│ DCP Sweep │")
75
+ lines.push("╰───────────────────────────────────────────────────────────╯")
76
+ lines.push("")
77
+ lines.push("Nothing swept: no user message found.")
78
+
79
+ return lines.join("\n")
80
+ }
81
+
82
+ function formatSweepMessage(
83
+ toolCount: number,
84
+ tokensSaved: number,
85
+ mode: "since-user" | "last-n",
86
+ toolIds: string[],
87
+ toolMetadata: Map<string, ToolParameterEntry>,
88
+ workingDirectory?: string,
89
+ skippedProtected?: number,
90
+ ): string {
91
+ const lines: string[] = []
92
+
93
+ lines.push("╭───────────────────────────────────────────────────────────╮")
94
+ lines.push("│ DCP Sweep │")
95
+ lines.push("╰───────────────────────────────────────────────────────────╯")
96
+ lines.push("")
97
+
98
+ if (toolCount === 0) {
99
+ if (mode === "since-user") {
100
+ lines.push("No tools found since the previous user message.")
101
+ } else {
102
+ lines.push(`No tools found to sweep.`)
103
+ }
104
+ if (skippedProtected && skippedProtected > 0) {
105
+ lines.push(`(${skippedProtected} protected tool(s) skipped)`)
106
+ }
107
+ } else {
108
+ if (mode === "since-user") {
109
+ lines.push(`Swept ${toolCount} tool(s) since the previous user message.`)
110
+ } else {
111
+ lines.push(`Swept the last ${toolCount} tool(s).`)
112
+ }
113
+ lines.push(`Tokens saved: ~${tokensSaved.toLocaleString()}`)
114
+ if (skippedProtected && skippedProtected > 0) {
115
+ lines.push(`(${skippedProtected} protected tool(s) skipped)`)
116
+ }
117
+ lines.push("")
118
+ const itemLines = formatPrunedItemsList(toolIds, toolMetadata, workingDirectory)
119
+ lines.push(...itemLines)
120
+ }
121
+
122
+ return lines.join("\n")
123
+ }
124
+
125
+ export async function handleSweepCommand(ctx: SweepCommandContext): Promise<void> {
126
+ const { client, state, config, logger, sessionId, messages, args, workingDirectory } = ctx
127
+
128
+ const params = getCurrentParams(state, messages, logger)
129
+ const protectedTools = config.commands.protectedTools
130
+
131
+ syncToolCache(state, config, logger, messages)
132
+ buildToolIdList(state, messages)
133
+
134
+ // Parse optional numeric argument
135
+ const numArg = args[0] ? parseInt(args[0], 10) : null
136
+ const isLastNMode = numArg !== null && !isNaN(numArg) && numArg > 0
137
+
138
+ let toolIdsToSweep: string[]
139
+ let mode: "since-user" | "last-n"
140
+
141
+ if (isLastNMode) {
142
+ // Mode: Sweep last N tools
143
+ mode = "last-n"
144
+ const startIndex = Math.max(0, state.toolIdList.length - numArg!)
145
+ toolIdsToSweep = state.toolIdList.slice(startIndex)
146
+ logger.info(`Sweep command: last ${numArg} mode, found ${toolIdsToSweep.length} tools`)
147
+ } else {
148
+ // Mode: Sweep since last user message
149
+ mode = "since-user"
150
+ const lastUserMsgIndex = findLastUserMessageIndex(messages)
151
+
152
+ if (lastUserMsgIndex === -1) {
153
+ // No user message found - show message and return
154
+ const message = formatNoUserMessage()
155
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
156
+ logger.info("Sweep command: no user message found")
157
+ return
158
+ } else {
159
+ toolIdsToSweep = collectToolIdsAfterIndex(state, messages, lastUserMsgIndex)
160
+ logger.info(
161
+ `Sweep command: found last user at index ${lastUserMsgIndex}, sweeping ${toolIdsToSweep.length} tools`,
162
+ )
163
+ }
164
+ }
165
+
166
+ // Filter out already-pruned tools, protected tools, and protected file paths
167
+ const newToolIds = toolIdsToSweep.filter((id) => {
168
+ if (state.prune.tools.has(id)) {
169
+ return false
170
+ }
171
+ const entry = state.toolParameters.get(id)
172
+ if (!entry) {
173
+ return true
174
+ }
175
+ if (
176
+ isToolProtected(
177
+ entry.tool,
178
+ entry.parameters,
179
+ protectedTools,
180
+ config.protectedFilePatterns,
181
+ entry.metadata,
182
+ )
183
+ ) {
184
+ logger.debug(`Sweep: skipping protected tool ${entry.tool} (${id})`)
185
+ return false
186
+ }
187
+ return true
188
+ })
189
+
190
+ // Count how many were skipped due to protection
191
+ const skippedProtected = toolIdsToSweep.filter((id) => {
192
+ const entry = state.toolParameters.get(id)
193
+ if (!entry) {
194
+ return false
195
+ }
196
+ return isToolProtected(
197
+ entry.tool,
198
+ entry.parameters,
199
+ protectedTools,
200
+ config.protectedFilePatterns,
201
+ entry.metadata,
202
+ )
203
+ }).length
204
+
205
+ if (newToolIds.length === 0) {
206
+ const message = formatSweepMessage(
207
+ 0,
208
+ 0,
209
+ mode,
210
+ [],
211
+ new Map(),
212
+ workingDirectory,
213
+ skippedProtected,
214
+ )
215
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
216
+ logger.info("Sweep command: no new tools to sweep", { skippedProtected })
217
+ return
218
+ }
219
+
220
+ const tokensSaved = getTotalToolTokens(state, newToolIds)
221
+
222
+ // Add to prune list
223
+ for (const id of newToolIds) {
224
+ const entry = state.toolParameters.get(id)
225
+ state.prune.tools.set(id, entry?.tokenCount ?? 0)
226
+ }
227
+ state.stats.pruneTokenCounter += tokensSaved
228
+ state.stats.totalPruneTokens += state.stats.pruneTokenCounter
229
+ state.stats.pruneTokenCounter = 0
230
+
231
+ // Collect metadata for logging
232
+ const toolMetadata: Map<string, ToolParameterEntry> = new Map()
233
+ for (const id of newToolIds) {
234
+ const entry = state.toolParameters.get(id)
235
+ if (entry) {
236
+ toolMetadata.set(id, entry)
237
+ }
238
+ }
239
+
240
+ // Persist state
241
+ saveSessionState(state, logger).catch((err) =>
242
+ logger.error("Failed to persist state after sweep", { error: err.message }),
243
+ )
244
+
245
+ const message = formatSweepMessage(
246
+ newToolIds.length,
247
+ tokensSaved,
248
+ mode,
249
+ newToolIds,
250
+ toolMetadata,
251
+ workingDirectory,
252
+ skippedProtected,
253
+ )
254
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
255
+
256
+ logger.info("Sweep command completed", {
257
+ toolsSwept: newToolIds.length,
258
+ tokensSaved,
259
+ skippedProtected,
260
+ mode,
261
+ tools: Array.from(toolMetadata.entries()).map(([id, entry]) => ({
262
+ id,
263
+ tool: entry.tool,
264
+ })),
265
+ })
266
+ }
@@ -0,0 +1,2 @@
1
+ export { ToolContext } from "./types"
2
+ export { createRcCompressTool } from "./rc"
@@ -0,0 +1,116 @@
1
+ import type { WithParts } from "../state"
2
+ import { ensureSessionInitialized, refreshManualMode } from "../state"
3
+ import { saveSessionState } from "../state/persistence"
4
+ import { assignMessageRefs } from "../message-ids"
5
+ import { isIgnoredUserMessage } from "../messages/query"
6
+ import { deduplicate, purgeErrors } from "../strategies"
7
+ import { getCurrentParams, getCurrentTokenUsage } from "../token-utils"
8
+ import { sendCompressNotification } from "../ui/notification"
9
+ import type { ToolContext } from "./types"
10
+ import { buildSearchContext, fetchSessionMessages } from "./search"
11
+ import type { SearchContext } from "./types"
12
+ import { applyPendingCompressionDurations } from "./timing"
13
+
14
+ interface RunContext {
15
+ ask(input: {
16
+ permission: string
17
+ patterns: string[]
18
+ always: string[]
19
+ metadata: Record<string, unknown>
20
+ }): Promise<void>
21
+ metadata(input: { title: string }): void
22
+ sessionID: string
23
+ }
24
+
25
+ export interface NotificationEntry {
26
+ blockId: number
27
+ runId: number
28
+ summary: string
29
+ summaryTokens: number
30
+ }
31
+
32
+ export interface PreparedSession {
33
+ rawMessages: WithParts[]
34
+ searchContext: SearchContext
35
+ }
36
+
37
+ export async function prepareSession(
38
+ ctx: ToolContext,
39
+ toolCtx: RunContext,
40
+ title: string,
41
+ ): Promise<PreparedSession> {
42
+ await refreshManualMode(ctx.state, toolCtx.sessionID, ctx.logger, ctx.config.manualMode.enabled)
43
+
44
+ if (ctx.state.manualMode && ctx.state.manualMode !== "compress-pending") {
45
+ throw new Error(
46
+ "Manual mode: compress blocked. Do not retry until `<compress triggered manually>` appears in user context.",
47
+ )
48
+ }
49
+
50
+ await toolCtx.ask({
51
+ permission: "compress",
52
+ patterns: ["*"],
53
+ always: ["*"],
54
+ metadata: {},
55
+ })
56
+
57
+ toolCtx.metadata({ title })
58
+
59
+ const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID)
60
+
61
+ await ensureSessionInitialized(
62
+ ctx.client,
63
+ ctx.state,
64
+ toolCtx.sessionID,
65
+ ctx.logger,
66
+ rawMessages,
67
+ ctx.config.manualMode.enabled,
68
+ )
69
+
70
+ assignMessageRefs(ctx.state, rawMessages)
71
+
72
+ deduplicate(ctx.state, ctx.logger, ctx.config, rawMessages)
73
+ purgeErrors(ctx.state, ctx.logger, ctx.config, rawMessages)
74
+
75
+ return {
76
+ rawMessages,
77
+ searchContext: buildSearchContext(ctx.state, rawMessages),
78
+ }
79
+ }
80
+
81
+ export async function finalizeSession(
82
+ ctx: ToolContext,
83
+ toolCtx: RunContext,
84
+ rawMessages: WithParts[],
85
+ entries: NotificationEntry[],
86
+ batchTopic: string | undefined,
87
+ ): Promise<void> {
88
+ if (ctx.state.manualMode === "compress-pending") {
89
+ ctx.state.manualMode = false
90
+ await refreshManualMode(
91
+ ctx.state,
92
+ toolCtx.sessionID,
93
+ ctx.logger,
94
+ ctx.config.manualMode.enabled,
95
+ )
96
+ }
97
+ applyPendingCompressionDurations(ctx.state)
98
+ await saveSessionState(ctx.state, ctx.logger)
99
+
100
+ const params = getCurrentParams(ctx.state, rawMessages, ctx.logger)
101
+ const sessionMessageIds = rawMessages
102
+ .filter((msg) => !isIgnoredUserMessage(msg))
103
+ .map((msg) => msg.info.id)
104
+
105
+ await sendCompressNotification(
106
+ ctx.client,
107
+ ctx.logger,
108
+ ctx.config,
109
+ ctx.state,
110
+ toolCtx.sessionID,
111
+ entries,
112
+ batchTopic,
113
+ sessionMessageIds,
114
+ params,
115
+ )
116
+ }
@@ -0,0 +1,163 @@
1
+ import type { SessionState } from "../state"
2
+ import { isIgnoredUserMessage } from "../messages/query"
3
+ import { isToolProtected } from "../protected-patterns"
4
+ import {
5
+ buildSubagentResultText,
6
+ getSubAgentId,
7
+ mergeSubagentResult,
8
+ } from "../subagents/subagent-results"
9
+ import { fetchSessionMessages } from "./search"
10
+ import type { SearchContext, SelectionResolution } from "./types"
11
+
12
+ export function appendProtectedPromptInfo(
13
+ summary: string,
14
+ selection: SelectionResolution,
15
+ searchContext: SearchContext,
16
+ state: SessionState,
17
+ enabled: boolean,
18
+ ): string {
19
+ if (!enabled) return summary
20
+
21
+ const protectedTexts: string[] = []
22
+
23
+ for (const messageId of selection.messageIds) {
24
+ const existingCompressionEntry = state.prune.messages.byMessageId.get(messageId)
25
+ if (existingCompressionEntry && existingCompressionEntry.activeBlockIds.length > 0) {
26
+ continue
27
+ }
28
+
29
+ const message = searchContext.rawMessagesById.get(messageId)
30
+ if (!message) continue
31
+ if (message.info.role !== "user") continue
32
+ if (isIgnoredUserMessage(message)) continue
33
+
34
+ const parts = Array.isArray(message.parts) ? message.parts : []
35
+ for (const part of parts) {
36
+ if (part.type !== "text" || typeof part.text !== "string") continue
37
+
38
+ protectedTexts.push(...extractProtectedPromptInfo(part.text))
39
+ }
40
+ }
41
+
42
+ if (protectedTexts.length === 0) {
43
+ return summary
44
+ }
45
+
46
+ const heading =
47
+ "\n\nThe following protected prompt information was included in this conversation verbatim:"
48
+ const body = protectedTexts.map((text) => `\n${text}`).join("")
49
+ return summary + heading + body
50
+ }
51
+
52
+ export function extractProtectedPromptInfo(text: string): string[] {
53
+ const protectedTexts: string[] = []
54
+ const protectTagRegex = /<protect>([\s\S]*?)<\/protect>/gi
55
+
56
+ for (const match of text.matchAll(protectTagRegex)) {
57
+ const protectedText = match[1]?.trim()
58
+ if (protectedText) {
59
+ protectedTexts.push(protectedText)
60
+ }
61
+ }
62
+
63
+ return protectedTexts
64
+ }
65
+
66
+ export async function appendProtectedTools(
67
+ client: any,
68
+ state: SessionState,
69
+ allowSubAgents: boolean,
70
+ summary: string,
71
+ selection: SelectionResolution,
72
+ searchContext: SearchContext,
73
+ protectedTools: string[],
74
+ protectedFilePatterns: string[] = [],
75
+ ): Promise<string> {
76
+ const protectedOutputs: string[] = []
77
+
78
+ for (const messageId of selection.messageIds) {
79
+ const existingCompressionEntry = state.prune.messages.byMessageId.get(messageId)
80
+ if (existingCompressionEntry && existingCompressionEntry.activeBlockIds.length > 0) {
81
+ continue
82
+ }
83
+
84
+ const message = searchContext.rawMessagesById.get(messageId)
85
+ if (!message) continue
86
+
87
+ const parts = Array.isArray(message.parts) ? message.parts : []
88
+ for (const part of parts) {
89
+ if (part.type === "tool" && part.callID) {
90
+ if (
91
+ isToolProtected(
92
+ part.tool,
93
+ part.state.input,
94
+ protectedTools,
95
+ protectedFilePatterns,
96
+ "metadata" in part.state ? part.state.metadata : undefined,
97
+ )
98
+ ) {
99
+ const title = `Tool: ${part.tool}`
100
+ let output = ""
101
+
102
+ if (part.state?.status === "completed" && part.state?.output) {
103
+ output =
104
+ typeof part.state.output === "string"
105
+ ? part.state.output
106
+ : JSON.stringify(part.state.output)
107
+ }
108
+
109
+ if (
110
+ allowSubAgents &&
111
+ (part.tool === "task" || part.tool === "subagent") &&
112
+ part.state?.status === "completed" &&
113
+ typeof part.state?.output === "string"
114
+ ) {
115
+ const cachedSubAgentResult = state.subAgentResultCache.get(part.callID)
116
+
117
+ if (cachedSubAgentResult !== undefined) {
118
+ if (cachedSubAgentResult) {
119
+ output = mergeSubagentResult(
120
+ part.state.output,
121
+ cachedSubAgentResult,
122
+ )
123
+ }
124
+ } else {
125
+ const subAgentSessionId = getSubAgentId(part)
126
+ if (subAgentSessionId) {
127
+ let subAgentResultText = ""
128
+ try {
129
+ const subAgentMessages = await fetchSessionMessages(
130
+ client,
131
+ subAgentSessionId,
132
+ )
133
+ subAgentResultText = buildSubagentResultText(subAgentMessages)
134
+ } catch {
135
+ subAgentResultText = ""
136
+ }
137
+
138
+ if (subAgentResultText) {
139
+ state.subAgentResultCache.set(part.callID, subAgentResultText)
140
+ output = mergeSubagentResult(
141
+ part.state.output,
142
+ subAgentResultText,
143
+ )
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ if (output) {
150
+ protectedOutputs.push(`\n### ${title}\n${output}`)
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ if (protectedOutputs.length === 0) {
158
+ return summary
159
+ }
160
+
161
+ const heading = "\n\nThe following protected tools were used in this conversation as well:"
162
+ return summary + heading + protectedOutputs.join("")
163
+ }