@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,448 @@
1
+ import type { Plugin } from "@opencode/plugin"
2
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin"
3
+ import { getConfig } from "../config"
4
+ import { Logger } from "../logger"
5
+ import { PromptStore } from "../prompts/store"
6
+ import { createRcCompressTool } from "../compress"
7
+ import { extractSummaryCallId, hasSummaryMarker } from "../compress/summary"
8
+ import { attachCompressionDuration } from "../compress/state"
9
+ import {
10
+ createCompressionUsageTracker,
11
+ installCompressionUsageHook,
12
+ installHttpUsageHook,
13
+ } from "./usage"
14
+ import { createCommandExecuteHandler, createSystemPromptHandler } from "../hooks"
15
+ import {
16
+ createSessionState,
17
+ ensureSessionInitialized,
18
+ checkSession,
19
+ saveSessionState,
20
+ syncToolCache,
21
+ type SessionState,
22
+ } from "../state"
23
+ import { assignMessageRefs } from "../message-ids"
24
+ import { applyPendingManualTrigger } from "../commands/manual"
25
+ import {
26
+ buildToolIdList,
27
+ injectCompressNudges,
28
+ injectMessageIds,
29
+ injectExtendedSubAgentResults,
30
+ prune,
31
+ stripHallucinations,
32
+ syncCompressionBlocks,
33
+ } from "../messages"
34
+ import { countTokens } from "../token-utils"
35
+ import { matchesGlob } from "../protected-patterns"
36
+ import { history, project } from "./messages"
37
+ import { analyzeContextTokens } from "../commands/context"
38
+ import { buildStatsReport } from "../commands/stats"
39
+ import { rpc } from "./rpc"
40
+
41
+ // Extension point for future model-invisible V2 reports. Never use synthetic()
42
+ // here: its text would enter the model's context, unlike V1 ignored messages.
43
+ export async function report(logger: Logger, text: string, sessionID?: string) {
44
+ logger.debug("V2 report (display pending)", { sessionID, text })
45
+ }
46
+
47
+ export async function setup(ctx: Plugin.Context) {
48
+ const warnings = {
49
+ tui: {
50
+ showToast: async (input: { body: { message: string } }) => {
51
+ console.warn(`DCP: ${input.body.message}`)
52
+ },
53
+ },
54
+ }
55
+ const config = getConfig({ directory: ctx.location.directory, client: warnings })
56
+ if (!config.enabled) {
57
+ await ctx.rpc.register(
58
+ { ...rpc, methods: { status: rpc.methods.status } },
59
+ { status: async () => ({ enabled: false }) },
60
+ )
61
+ return
62
+ }
63
+ const logger = new Logger(config.debug)
64
+ const usageTracker = createCompressionUsageTracker()
65
+ await installCompressionUsageHook(ctx, usageTracker, logger)
66
+ await installHttpUsageHook(ctx, usageTracker, logger)
67
+ const prompts = new PromptStore(
68
+ logger,
69
+ ctx.location.directory,
70
+ config.experimental.customPrompts,
71
+ "compact",
72
+ )
73
+ const sessions = new Map<string, SessionState>()
74
+ const queues = new Map<string, Promise<unknown>>()
75
+ const limits = new Map<string, number>()
76
+ const aliases: Record<string, string> = {
77
+ task: "subagent",
78
+ bash: "shell",
79
+ apply_patch: "patch",
80
+ }
81
+ for (const list of [
82
+ config.compress.protectedTools,
83
+ config.commands.protectedTools,
84
+ config.strategies.deduplication.protectedTools,
85
+ config.strategies.purgeErrors.protectedTools,
86
+ ]) {
87
+ for (const name of [...list])
88
+ if (aliases[name] && !list.includes(aliases[name]!)) list.push(aliases[name]!)
89
+ }
90
+
91
+ function serial<T>(sessionID: string, operation: () => Promise<T>): Promise<T> {
92
+ const pending = (queues.get(sessionID) ?? Promise.resolve()).catch(() => {}).then(operation)
93
+ queues.set(sessionID, pending)
94
+ void pending
95
+ .finally(() => {
96
+ if (queues.get(sessionID) === pending) queues.delete(sessionID)
97
+ })
98
+ .catch(() => {})
99
+ return pending
100
+ }
101
+
102
+ const client = {
103
+ session: {
104
+ get: async ({ path }: { path: { id: string } }) => ({
105
+ data: await ctx.session.get({ sessionID: path.id }),
106
+ }),
107
+ messages: async ({ path }: { path: { id: string } }) => {
108
+ const [entries, session] = await Promise.all([
109
+ ctx.session.context({ sessionID: path.id }),
110
+ ctx.session.get({ sessionID: path.id }),
111
+ ])
112
+ return { data: history(entries, session) }
113
+ },
114
+ prompt: async (input: {
115
+ path: { id: string }
116
+ body: { parts: Array<{ text: string }> }
117
+ }) =>
118
+ report(logger, input.body.parts.map((part) => part.text).join("\n"), input.path.id),
119
+ },
120
+ tui: {
121
+ showToast: async (input: { body: { message: string } }) =>
122
+ report(logger, input.body.message),
123
+ },
124
+ }
125
+
126
+ async function load(sessionID: string, agentID?: string) {
127
+ const [session, entries] = await Promise.all([
128
+ ctx.session.get({ sessionID }),
129
+ ctx.session.context({ sessionID }),
130
+ ])
131
+ const selected =
132
+ agentID ??
133
+ session.agent ??
134
+ entries.findLast((entry) => entry.type === "assistant")?.agent
135
+ if (!selected) throw new Error("DCP commands require a session with a selected agent")
136
+ const { data: agent } = await ctx.agent.get({ agentID: selected })
137
+ let state = sessions.get(sessionID)
138
+ if (!state) {
139
+ state = createSessionState("compact")
140
+ sessions.set(sessionID, state)
141
+ }
142
+ const messages = history(entries, session)
143
+ await ensureSessionInitialized(
144
+ client,
145
+ state,
146
+ sessionID,
147
+ logger,
148
+ messages,
149
+ config.manualMode.enabled,
150
+ )
151
+ await checkSession(client, state, logger, messages, config.manualMode.enabled)
152
+ const rule = [...agent.permissions, ...(session.permissions ?? [])].findLast(
153
+ (rule) => matchesGlob("compress", rule.action) && matchesGlob("*", rule.resource),
154
+ )
155
+ state.compressPermission =
156
+ config.compress.permission === "deny"
157
+ ? "deny"
158
+ : rule?.effect === "deny"
159
+ ? "deny"
160
+ : config.compress.permission === "ask" || rule?.effect === "ask"
161
+ ? "ask"
162
+ : "allow"
163
+ return { state, entries, session, messages }
164
+ }
165
+
166
+ function allowed(state: SessionState) {
167
+ if (state.isSubAgent && !config.experimental.allowSubAgents)
168
+ throw new Error("DCP compression is disabled in subagents")
169
+ if (state.compressPermission === "deny") throw new Error("DCP compression is denied")
170
+ if (state.compressPermission === "ask")
171
+ throw new Error(
172
+ "DCP: compress permission 'ask' is not supported by OpenCode V2's public plugin API yet. Compression was not performed.",
173
+ )
174
+ }
175
+
176
+ await ctx.model.transform((editor) => {
177
+ limits.clear()
178
+ for (const model of editor.list())
179
+ limits.set(`${model.providerID}/${model.id}`, model.limit.context)
180
+ })
181
+ for (const kind of ["context", "compaction"] as const)
182
+ await ctx.session.hook(kind, (event) =>
183
+ serial(event.sessionID, async () => {
184
+ const { state, entries, session, messages } = await load(
185
+ event.sessionID,
186
+ event.agent,
187
+ )
188
+ if (state.isSubAgent && !config.experimental.allowSubAgents) {
189
+ delete event.tools.compress
190
+ return
191
+ }
192
+ if (state.compressPermission === "deny") delete event.tools.compress
193
+ state.modelContextLimit = limits.get(`${event.model.providerID}/${event.model.id}`)
194
+ state.systemPromptTokens = countTokens(
195
+ event.system.map((part) => part.text).join("\n"),
196
+ )
197
+ const view = project(event.messages, entries, {
198
+ ...session,
199
+ agent: event.agent,
200
+ model: event.model,
201
+ })
202
+ stripHallucinations(view.messages, state.idFormat)
203
+ assignMessageRefs(state, view.messages)
204
+ // Compaction may select only a prefix; block origins can be in the retained tail.
205
+ syncCompressionBlocks(state, logger, messages)
206
+ syncToolCache(state, config, logger, view.messages)
207
+ buildToolIdList(state, view.messages)
208
+ prune(state, logger, view.messages, view.summaryBase)
209
+ await injectExtendedSubAgentResults(
210
+ client,
211
+ state,
212
+ logger,
213
+ view.messages,
214
+ config.experimental.allowSubAgents,
215
+ )
216
+ prompts.reload()
217
+ injectCompressNudges(
218
+ state,
219
+ config,
220
+ logger,
221
+ view.messages,
222
+ prompts.getRuntimePrompts(),
223
+ kind === "context",
224
+ )
225
+ injectMessageIds(state, config, view.messages)
226
+ applyPendingManualTrigger(state, view.messages, logger)
227
+ event.messages = view.restore()
228
+ const system = { system: event.system.map((part) => part.text) }
229
+ await createSystemPromptHandler(
230
+ state,
231
+ logger,
232
+ config,
233
+ prompts,
234
+ )(
235
+ {
236
+ sessionID: event.sessionID,
237
+ model: { limit: { context: state.modelContextLimit ?? 0 } },
238
+ },
239
+ system,
240
+ )
241
+ event.system = system.system.map((text, index) => ({
242
+ ...event.system[index],
243
+ type: "text",
244
+ text,
245
+ }))
246
+ await logger.saveContext(event.sessionID, view.messages)
247
+ }),
248
+ )
249
+
250
+ // Hidden summary generation for rc mode: when our marker is present, drop
251
+ // the session context and every tool so the transient request contains
252
+ // only the prompt we built. This keeps the summarization request invisible
253
+ // and independent of any injected metadata.
254
+ await ctx.session.hook("generate", (event) => {
255
+ const own = event.messages.filter(
256
+ (message) =>
257
+ Array.isArray(message.content) &&
258
+ message.content.some(
259
+ (part: any) =>
260
+ part?.type === "text" &&
261
+ typeof part.text === "string" &&
262
+ hasSummaryMarker(part.text),
263
+ ),
264
+ )
265
+ if (own.length === 0) {
266
+ return
267
+ }
268
+ event.messages = own as typeof event.messages
269
+ for (const name of Object.keys(event.tools ?? {})) {
270
+ delete (event.tools as Record<string, unknown>)[name]
271
+ }
272
+
273
+ // Local fallback estimate of the request input, used only when the
274
+ // provider usage cannot be captured from the language model wrapper.
275
+ const callId = own
276
+ .flatMap((message) =>
277
+ Array.isArray(message.content) ? (message.content as any[]) : [],
278
+ )
279
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
280
+ .map((part) => extractSummaryCallId(part.text))
281
+ .find((id): id is string => !!id)
282
+ if (callId) {
283
+ const texts: string[] = []
284
+ for (const part of event.system ?? []) {
285
+ if (typeof (part as any)?.text === "string") {
286
+ texts.push((part as any).text)
287
+ }
288
+ }
289
+ for (const message of own) {
290
+ for (const part of (message.content as any[]) ?? []) {
291
+ if (part?.type === "text" && typeof part.text === "string") {
292
+ texts.push(part.text)
293
+ }
294
+ }
295
+ }
296
+ usageTracker.recordEstimatedInput(callId, countTokens(texts.join("\n")))
297
+ }
298
+ })
299
+
300
+ if (config.compress.permission !== "deny") {
301
+ const define = (state: SessionState): ToolDefinition => {
302
+ const context = {
303
+ client,
304
+ state,
305
+ logger,
306
+ config,
307
+ prompts,
308
+ generate: async (input: { sessionID: string; prompt: string; callId?: string }) => {
309
+ try {
310
+ const result = await ctx.session.generate({
311
+ sessionID: input.sessionID,
312
+ prompt: input.prompt,
313
+ })
314
+ const text = result?.text ?? ""
315
+ const usage = input.callId
316
+ ? usageTracker.resolve(input.callId, text)
317
+ : undefined
318
+ return { text, usage }
319
+ } finally {
320
+ if (input.callId) {
321
+ usageTracker.discard(input.callId)
322
+ }
323
+ }
324
+ },
325
+ }
326
+ return createRcCompressTool(context)
327
+ }
328
+ const definition = define(createSessionState("compact"))
329
+ await ctx.tool.transform((editor) =>
330
+ editor.add({
331
+ name: "compress",
332
+ description: definition.description,
333
+ input: tool.schema.object(definition.args),
334
+ options: { codemode: false, permission: "compress" },
335
+ execute: (input, context) =>
336
+ serial(context.sessionID, async () => {
337
+ const { state } = await load(context.sessionID, context.agent)
338
+ allowed(state)
339
+ const started = Date.now()
340
+ const legacy = define(state)
341
+ const content = await legacy.execute(input, {
342
+ sessionID: context.sessionID,
343
+ messageID: context.messageID,
344
+ callID: context.id,
345
+ agent: context.agent,
346
+ directory: ctx.location.directory,
347
+ worktree: ctx.location.directory,
348
+ abort: new AbortController().signal,
349
+ ask: async () => allowed(state),
350
+ metadata: ({ title }: { title?: string }) => {
351
+ void context.progress({ title })
352
+ },
353
+ } as Parameters<typeof legacy.execute>[1])
354
+ attachCompressionDuration(
355
+ state.prune.messages,
356
+ context.messageID,
357
+ context.id,
358
+ Date.now() - started,
359
+ )
360
+ await saveSessionState(state, logger)
361
+ // The rc executor returns the tool result text.
362
+ return { content: content as string }
363
+ }),
364
+ }),
365
+ )
366
+ }
367
+ if (config.commands.enabled)
368
+ await ctx.command.transform((editor) => {
369
+ for (const name of ["dcp", "dcp-compress"])
370
+ editor.add({
371
+ name,
372
+ description: name === "dcp" ? "DCP commands" : "Trigger DCP manual compression",
373
+ execute: async (invocation) => {
374
+ const prompt = await serial(invocation.sessionID, async () => {
375
+ const { state } = await load(invocation.sessionID)
376
+ const permission =
377
+ state.compressPermission ?? config.compress.permission
378
+ if (
379
+ name === "dcp-compress" ||
380
+ invocation.prompt.text.trim().split(/\s+/)[0] === "compress"
381
+ )
382
+ allowed(state)
383
+ const output = { parts: [] }
384
+ await createCommandExecuteHandler(
385
+ client,
386
+ state,
387
+ logger,
388
+ config,
389
+ ctx.location.directory,
390
+ )(
391
+ {
392
+ command: name,
393
+ sessionID: invocation.sessionID,
394
+ arguments: invocation.prompt.text,
395
+ },
396
+ output,
397
+ )
398
+ state.compressPermission = permission
399
+ const pending = state.pendingManualTrigger
400
+ if (!pending) return
401
+ allowed(state)
402
+ state.pendingManualTrigger = null
403
+ return pending.prompt
404
+ })
405
+ if (prompt)
406
+ await ctx.session.prompt({
407
+ sessionID: invocation.sessionID,
408
+ text: prompt,
409
+ delivery: invocation.delivery,
410
+ })
411
+ },
412
+ })
413
+ })
414
+ await ctx.rpc.register(rpc, {
415
+ status: async () => ({ enabled: config.commands.enabled }),
416
+ snapshot: ({ sessionID }) =>
417
+ serial(sessionID, async () => {
418
+ const { state, messages } = await load(sessionID)
419
+ syncCompressionBlocks(state, logger, messages)
420
+ return {
421
+ manualMode: !!state.manualMode,
422
+ canCompress:
423
+ state.compressPermission === "allow" &&
424
+ (!state.isSubAgent || config.experimental.allowSubAgents),
425
+ ...(state.compressPermission === "ask"
426
+ ? {
427
+ blockedReason:
428
+ "Permission 'ask' is not supported by the V2 plugin API yet.",
429
+ }
430
+ : {}),
431
+ context: analyzeContextTokens(state, messages),
432
+ stats: await buildStatsReport(state, logger),
433
+ }
434
+ }),
435
+ manual: ({ sessionID, enabled }) =>
436
+ serial(sessionID, async () => {
437
+ const { state } = await load(sessionID)
438
+ state.manualMode = enabled ? "active" : false
439
+ await saveSessionState(state, logger)
440
+ return {}
441
+ }),
442
+ })
443
+ logger.info("DCP V2 initialized")
444
+ return () => {
445
+ sessions.clear()
446
+ limits.clear()
447
+ }
448
+ }