@tarquinen/opencode-dcp 3.2.5-beta0 → 3.2.6-beta0

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 (72) hide show
  1. package/index.ts +141 -0
  2. package/lib/analysis/tokens.ts +225 -0
  3. package/lib/auth.ts +37 -0
  4. package/lib/commands/compression-targets.ts +137 -0
  5. package/lib/commands/context.ts +132 -0
  6. package/lib/commands/decompress.ts +275 -0
  7. package/lib/commands/help.ts +76 -0
  8. package/lib/commands/index.ts +11 -0
  9. package/lib/commands/manual.ts +125 -0
  10. package/lib/commands/recompress.ts +224 -0
  11. package/lib/commands/stats.ts +148 -0
  12. package/lib/commands/sweep.ts +268 -0
  13. package/lib/compress/index.ts +3 -0
  14. package/lib/compress/message-utils.ts +250 -0
  15. package/lib/compress/message.ts +137 -0
  16. package/lib/compress/pipeline.ts +106 -0
  17. package/lib/compress/protected-content.ts +154 -0
  18. package/lib/compress/range-utils.ts +308 -0
  19. package/lib/compress/range.ts +180 -0
  20. package/lib/compress/search.ts +267 -0
  21. package/lib/compress/state.ts +268 -0
  22. package/lib/compress/timing.ts +77 -0
  23. package/lib/compress/types.ts +108 -0
  24. package/lib/compress-permission.ts +25 -0
  25. package/lib/config.ts +1071 -0
  26. package/lib/hooks.ts +378 -0
  27. package/lib/host-permissions.ts +101 -0
  28. package/lib/logger.ts +235 -0
  29. package/lib/message-ids.ts +172 -0
  30. package/lib/messages/index.ts +8 -0
  31. package/lib/messages/inject/inject.ts +215 -0
  32. package/lib/messages/inject/subagent-results.ts +82 -0
  33. package/lib/messages/inject/utils.ts +374 -0
  34. package/lib/messages/priority.ts +102 -0
  35. package/lib/messages/prune.ts +238 -0
  36. package/lib/messages/query.ts +56 -0
  37. package/lib/messages/reasoning-strip.ts +40 -0
  38. package/lib/messages/sync.ts +124 -0
  39. package/lib/messages/utils.ts +187 -0
  40. package/lib/prompts/compress-message.ts +42 -0
  41. package/lib/prompts/compress-range.ts +60 -0
  42. package/lib/prompts/context-limit-nudge.ts +18 -0
  43. package/lib/prompts/extensions/nudge.ts +43 -0
  44. package/lib/prompts/extensions/system.ts +32 -0
  45. package/lib/prompts/extensions/tool.ts +35 -0
  46. package/lib/prompts/index.ts +29 -0
  47. package/lib/prompts/iteration-nudge.ts +6 -0
  48. package/lib/prompts/store.ts +467 -0
  49. package/lib/prompts/system.ts +33 -0
  50. package/lib/prompts/turn-nudge.ts +10 -0
  51. package/lib/protected-patterns.ts +128 -0
  52. package/lib/state/index.ts +4 -0
  53. package/lib/state/persistence.ts +256 -0
  54. package/lib/state/state.ts +190 -0
  55. package/lib/state/tool-cache.ts +98 -0
  56. package/lib/state/types.ts +112 -0
  57. package/lib/state/utils.ts +334 -0
  58. package/lib/strategies/deduplication.ts +127 -0
  59. package/lib/strategies/index.ts +2 -0
  60. package/lib/strategies/purge-errors.ts +88 -0
  61. package/lib/subagents/subagent-results.ts +74 -0
  62. package/lib/token-utils.ts +162 -0
  63. package/lib/ui/notification.ts +346 -0
  64. package/lib/ui/utils.ts +287 -0
  65. package/package.json +9 -2
  66. package/tui/data/context.ts +177 -0
  67. package/tui/index.tsx +34 -0
  68. package/tui/routes/summary.tsx +175 -0
  69. package/tui/shared/names.ts +9 -0
  70. package/tui/shared/theme.ts +58 -0
  71. package/tui/shared/types.ts +38 -0
  72. package/tui/slots/sidebar-content.tsx +502 -0
@@ -0,0 +1,250 @@
1
+ import type { PluginConfig } from "../config"
2
+ import type { SessionState } from "../state"
3
+ import { parseBoundaryId } from "../message-ids"
4
+ import { isIgnoredUserMessage, isProtectedUserMessage } from "../messages/query"
5
+ import { resolveAnchorMessageId, resolveBoundaryIds, resolveSelection } from "./search"
6
+ import { COMPRESSED_BLOCK_HEADER } from "./state"
7
+ import type {
8
+ CompressMessageEntry,
9
+ CompressMessageToolArgs,
10
+ ResolvedMessageCompression,
11
+ ResolvedMessageCompressionsResult,
12
+ SearchContext,
13
+ } from "./types"
14
+
15
+ interface SkippedIssue {
16
+ kind: string
17
+ messageId: string
18
+ }
19
+
20
+ class SoftIssue extends Error {
21
+ constructor(
22
+ public readonly kind: string,
23
+ public readonly messageId: string,
24
+ message: string,
25
+ ) {
26
+ super(message)
27
+ }
28
+ }
29
+
30
+ export function validateArgs(args: CompressMessageToolArgs): void {
31
+ if (typeof args.topic !== "string" || args.topic.trim().length === 0) {
32
+ throw new Error("topic is required and must be a non-empty string")
33
+ }
34
+
35
+ if (!Array.isArray(args.content) || args.content.length === 0) {
36
+ throw new Error("content is required and must be a non-empty array")
37
+ }
38
+
39
+ for (let index = 0; index < args.content.length; index++) {
40
+ const entry = args.content[index]
41
+ const prefix = `content[${index}]`
42
+
43
+ if (typeof entry?.messageId !== "string" || entry.messageId.trim().length === 0) {
44
+ throw new Error(`${prefix}.messageId is required and must be a non-empty string`)
45
+ }
46
+
47
+ if (typeof entry?.topic !== "string" || entry.topic.trim().length === 0) {
48
+ throw new Error(`${prefix}.topic is required and must be a non-empty string`)
49
+ }
50
+
51
+ if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
52
+ throw new Error(`${prefix}.summary is required and must be a non-empty string`)
53
+ }
54
+ }
55
+ }
56
+
57
+ export function formatResult(
58
+ processedCount: number,
59
+ skippedIssues: string[],
60
+ skippedCount: number,
61
+ ): string {
62
+ const messageNoun = processedCount === 1 ? "message" : "messages"
63
+ const processedText =
64
+ processedCount > 0
65
+ ? `Compressed ${processedCount} ${messageNoun} into ${COMPRESSED_BLOCK_HEADER}.`
66
+ : "Compressed 0 messages."
67
+
68
+ if (skippedCount === 0) {
69
+ return processedText
70
+ }
71
+
72
+ const issueNoun = skippedCount === 1 ? "issue" : "issues"
73
+ const issueLines = skippedIssues.map((issue) => `- ${issue}`).join("\n")
74
+ return `${processedText}\nSkipped ${skippedCount} ${issueNoun}:\n${issueLines}`
75
+ }
76
+
77
+ export function formatIssues(skippedIssues: string[], skippedCount: number): string {
78
+ const issueNoun = skippedCount === 1 ? "issue" : "issues"
79
+ const issueLines = skippedIssues.map((issue) => `- ${issue}`).join("\n")
80
+ return `Unable to compress any messages. Found ${skippedCount} ${issueNoun}:\n${issueLines}`
81
+ }
82
+
83
+ const ISSUE_TEMPLATES: Record<string, [singular: string, plural: string]> = {
84
+ blocked: [
85
+ "refers to a protected message and cannot be compressed.",
86
+ "refer to protected messages and cannot be compressed.",
87
+ ],
88
+ "invalid-format": [
89
+ "is invalid. Use an injected raw message ID of the form mNNNN.",
90
+ "are invalid. Use injected raw message IDs of the form mNNNN.",
91
+ ],
92
+ "block-id": [
93
+ "is invalid here. Block IDs like bN are not allowed; use an mNNNN message ID instead.",
94
+ "are invalid here. Block IDs like bN are not allowed; use mNNNN message IDs instead.",
95
+ ],
96
+ "not-in-context": [
97
+ "is not available in the current conversation context. Choose an injected mNNNN ID visible in context.",
98
+ "are not available in the current conversation context. Choose injected mNNNN IDs visible in context.",
99
+ ],
100
+ protected: [
101
+ "refers to a protected message and cannot be compressed.",
102
+ "refer to protected messages and cannot be compressed.",
103
+ ],
104
+ "already-compressed": [
105
+ "is already part of an active compression.",
106
+ "are already part of active compressions.",
107
+ ],
108
+ duplicate: [
109
+ "was selected more than once in this batch.",
110
+ "were each selected more than once in this batch.",
111
+ ],
112
+ }
113
+
114
+ function formatSkippedGroup(kind: string, messageIds: string[]): string {
115
+ const templates = ISSUE_TEMPLATES[kind]
116
+ const ids = messageIds.join(", ")
117
+ const single = messageIds.length === 1
118
+ const prefix = single ? "messageId" : "messageIds"
119
+
120
+ if (!templates) {
121
+ return `${prefix} ${ids}: unknown issue.`
122
+ }
123
+
124
+ return `${prefix} ${ids} ${single ? templates[0] : templates[1]}`
125
+ }
126
+
127
+ function groupSkippedIssues(issues: SkippedIssue[]): string[] {
128
+ const groups = new Map<string, string[]>()
129
+ const order: string[] = []
130
+
131
+ for (const issue of issues) {
132
+ let ids = groups.get(issue.kind)
133
+ if (!ids) {
134
+ ids = []
135
+ groups.set(issue.kind, ids)
136
+ order.push(issue.kind)
137
+ }
138
+ ids.push(issue.messageId)
139
+ }
140
+
141
+ return order.map((kind) => {
142
+ const ids = groups.get(kind)!
143
+ return formatSkippedGroup(kind, ids)
144
+ })
145
+ }
146
+
147
+ export function resolveMessages(
148
+ args: CompressMessageToolArgs,
149
+ searchContext: SearchContext,
150
+ state: SessionState,
151
+ config: PluginConfig,
152
+ ): ResolvedMessageCompressionsResult {
153
+ const issues: SkippedIssue[] = []
154
+ const plans: ResolvedMessageCompression[] = []
155
+ const seenMessageIds = new Set<string>()
156
+
157
+ for (const entry of args.content) {
158
+ const normalizedMessageId = entry.messageId.trim()
159
+ if (seenMessageIds.has(normalizedMessageId)) {
160
+ issues.push({ kind: "duplicate", messageId: normalizedMessageId })
161
+ continue
162
+ }
163
+
164
+ try {
165
+ const plan = resolveMessage(
166
+ {
167
+ ...entry,
168
+ messageId: normalizedMessageId,
169
+ },
170
+ searchContext,
171
+ state,
172
+ config,
173
+ )
174
+ seenMessageIds.add(plan.entry.messageId)
175
+ plans.push(plan)
176
+ } catch (error: any) {
177
+ if (error instanceof SoftIssue) {
178
+ issues.push({ kind: error.kind, messageId: error.messageId })
179
+ continue
180
+ }
181
+
182
+ throw error
183
+ }
184
+ }
185
+
186
+ return {
187
+ plans,
188
+ skippedIssues: groupSkippedIssues(issues),
189
+ skippedCount: issues.length,
190
+ }
191
+ }
192
+
193
+ function resolveMessage(
194
+ entry: CompressMessageEntry,
195
+ searchContext: SearchContext,
196
+ state: SessionState,
197
+ config: PluginConfig,
198
+ ): ResolvedMessageCompression {
199
+ if (entry.messageId.toUpperCase() === "BLOCKED") {
200
+ throw new SoftIssue("blocked", "BLOCKED", "protected message")
201
+ }
202
+
203
+ const parsed = parseBoundaryId(entry.messageId)
204
+
205
+ if (!parsed) {
206
+ throw new SoftIssue("invalid-format", entry.messageId, "invalid format")
207
+ }
208
+
209
+ if (parsed.kind === "compressed-block") {
210
+ throw new SoftIssue("block-id", entry.messageId, "block ID used")
211
+ }
212
+
213
+ const messageId = state.messageIds.byRef.get(parsed.ref)
214
+ const rawMessage = messageId ? searchContext.rawMessagesById.get(messageId) : undefined
215
+ if (
216
+ !messageId ||
217
+ !rawMessage ||
218
+ !searchContext.rawIndexById.has(messageId) ||
219
+ isIgnoredUserMessage(rawMessage)
220
+ ) {
221
+ throw new SoftIssue("not-in-context", parsed.ref, "not in context")
222
+ }
223
+
224
+ const { startReference, endReference } = resolveBoundaryIds(
225
+ searchContext,
226
+ state,
227
+ parsed.ref,
228
+ parsed.ref,
229
+ )
230
+ const selection = resolveSelection(searchContext, startReference, endReference)
231
+
232
+ if (isProtectedUserMessage(config, rawMessage)) {
233
+ throw new SoftIssue("protected", parsed.ref, "protected message")
234
+ }
235
+
236
+ const pruneEntry = state.prune.messages.byMessageId.get(messageId)
237
+ if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
238
+ throw new SoftIssue("already-compressed", parsed.ref, "already compressed")
239
+ }
240
+
241
+ return {
242
+ entry: {
243
+ messageId: parsed.ref,
244
+ topic: entry.topic,
245
+ summary: entry.summary,
246
+ },
247
+ selection,
248
+ anchorMessageId: resolveAnchorMessageId(startReference),
249
+ }
250
+ }
@@ -0,0 +1,137 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import type { ToolContext } from "./types"
3
+ import { countTokens } from "../token-utils"
4
+ import { MESSAGE_FORMAT_EXTENSION } from "../prompts/extensions/tool"
5
+ import { formatIssues, formatResult, resolveMessages, validateArgs } from "./message-utils"
6
+ import { finalizeSession, prepareSession, type NotificationEntry } from "./pipeline"
7
+ import { appendProtectedTools } from "./protected-content"
8
+ import {
9
+ allocateBlockId,
10
+ allocateRunId,
11
+ applyCompressionState,
12
+ wrapCompressedSummary,
13
+ } from "./state"
14
+ import type { CompressMessageToolArgs } from "./types"
15
+
16
+ function buildSchema() {
17
+ return {
18
+ topic: tool.schema
19
+ .string()
20
+ .describe(
21
+ "Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'",
22
+ ),
23
+ content: tool.schema
24
+ .array(
25
+ tool.schema.object({
26
+ messageId: tool.schema
27
+ .string()
28
+ .describe("Raw message ID to compress (e.g. m0001)"),
29
+ topic: tool.schema
30
+ .string()
31
+ .describe("Short label (3-5 words) for this one message summary"),
32
+ summary: tool.schema
33
+ .string()
34
+ .describe("Complete technical summary replacing that one message"),
35
+ }),
36
+ )
37
+ .describe("Batch of individual message summaries to create in one tool call"),
38
+ }
39
+ }
40
+
41
+ export function createCompressMessageTool(ctx: ToolContext): ReturnType<typeof tool> {
42
+ ctx.prompts.reload()
43
+ const runtimePrompts = ctx.prompts.getRuntimePrompts()
44
+
45
+ return tool({
46
+ description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
47
+ args: buildSchema(),
48
+ async execute(args, toolCtx) {
49
+ const input = args as CompressMessageToolArgs
50
+ validateArgs(input)
51
+ const callId =
52
+ typeof (toolCtx as unknown as { callID?: unknown }).callID === "string"
53
+ ? (toolCtx as unknown as { callID: string }).callID
54
+ : undefined
55
+
56
+ const { rawMessages, searchContext } = await prepareSession(
57
+ ctx,
58
+ toolCtx,
59
+ `Compress Message: ${input.topic}`,
60
+ )
61
+ const { plans, skippedIssues, skippedCount } = resolveMessages(
62
+ input,
63
+ searchContext,
64
+ ctx.state,
65
+ ctx.config,
66
+ )
67
+
68
+ if (plans.length === 0 && skippedCount > 0) {
69
+ throw new Error(formatIssues(skippedIssues, skippedCount))
70
+ }
71
+
72
+ const notifications: NotificationEntry[] = []
73
+
74
+ const preparedPlans: Array<{
75
+ plan: (typeof plans)[number]
76
+ summaryWithTools: string
77
+ }> = []
78
+
79
+ for (const plan of plans) {
80
+ const summaryWithTools = await appendProtectedTools(
81
+ ctx.client,
82
+ ctx.state,
83
+ ctx.config.experimental.allowSubAgents,
84
+ plan.entry.summary,
85
+ plan.selection,
86
+ searchContext,
87
+ ctx.config.compress.protectedTools,
88
+ ctx.config.protectedFilePatterns,
89
+ )
90
+
91
+ preparedPlans.push({
92
+ plan,
93
+ summaryWithTools,
94
+ })
95
+ }
96
+
97
+ const runId = allocateRunId(ctx.state)
98
+
99
+ for (const { plan, summaryWithTools } of preparedPlans) {
100
+ const blockId = allocateBlockId(ctx.state)
101
+ const storedSummary = wrapCompressedSummary(blockId, summaryWithTools)
102
+ const summaryTokens = countTokens(storedSummary)
103
+
104
+ applyCompressionState(
105
+ ctx.state,
106
+ {
107
+ topic: plan.entry.topic,
108
+ batchTopic: input.topic,
109
+ startId: plan.entry.messageId,
110
+ endId: plan.entry.messageId,
111
+ mode: "message",
112
+ runId,
113
+ compressMessageId: toolCtx.messageID,
114
+ compressCallId: callId,
115
+ summaryTokens,
116
+ },
117
+ plan.selection,
118
+ plan.anchorMessageId,
119
+ blockId,
120
+ storedSummary,
121
+ [],
122
+ )
123
+
124
+ notifications.push({
125
+ blockId,
126
+ runId,
127
+ summary: summaryWithTools,
128
+ summaryTokens,
129
+ })
130
+ }
131
+
132
+ await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic)
133
+
134
+ return formatResult(plans.length, skippedIssues, skippedCount)
135
+ },
136
+ })
137
+ }
@@ -0,0 +1,106 @@
1
+ import type { WithParts } from "../state"
2
+ import { ensureSessionInitialized } 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 } 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
+ if (ctx.state.manualMode && ctx.state.manualMode !== "compress-pending") {
43
+ throw new Error(
44
+ "Manual mode: compress blocked. Do not retry until `<compress triggered manually>` appears in user context.",
45
+ )
46
+ }
47
+
48
+ await toolCtx.ask({
49
+ permission: "compress",
50
+ patterns: ["*"],
51
+ always: ["*"],
52
+ metadata: {},
53
+ })
54
+
55
+ toolCtx.metadata({ title })
56
+
57
+ const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID)
58
+
59
+ await ensureSessionInitialized(
60
+ ctx.client,
61
+ ctx.state,
62
+ toolCtx.sessionID,
63
+ ctx.logger,
64
+ rawMessages,
65
+ ctx.config.manualMode.enabled,
66
+ )
67
+
68
+ assignMessageRefs(ctx.state, rawMessages)
69
+
70
+ deduplicate(ctx.state, ctx.logger, ctx.config, rawMessages)
71
+ purgeErrors(ctx.state, ctx.logger, ctx.config, rawMessages)
72
+
73
+ return {
74
+ rawMessages,
75
+ searchContext: buildSearchContext(ctx.state, rawMessages),
76
+ }
77
+ }
78
+
79
+ export async function finalizeSession(
80
+ ctx: ToolContext,
81
+ toolCtx: RunContext,
82
+ rawMessages: WithParts[],
83
+ entries: NotificationEntry[],
84
+ batchTopic: string | undefined,
85
+ ): Promise<void> {
86
+ ctx.state.manualMode = ctx.state.manualMode ? "active" : false
87
+ applyPendingCompressionDurations(ctx.state)
88
+ await saveSessionState(ctx.state, ctx.logger)
89
+
90
+ const params = getCurrentParams(ctx.state, rawMessages, ctx.logger)
91
+ const sessionMessageIds = rawMessages
92
+ .filter((msg) => !isIgnoredUserMessage(msg))
93
+ .map((msg) => msg.info.id)
94
+
95
+ await sendCompressNotification(
96
+ ctx.client,
97
+ ctx.logger,
98
+ ctx.config,
99
+ ctx.state,
100
+ toolCtx.sessionID,
101
+ entries,
102
+ batchTopic,
103
+ sessionMessageIds,
104
+ params,
105
+ )
106
+ }
@@ -0,0 +1,154 @@
1
+ import type { SessionState } from "../state"
2
+ import { isIgnoredUserMessage } from "../messages/query"
3
+ import {
4
+ getFilePathsFromParameters,
5
+ isFilePathProtected,
6
+ isToolNameProtected,
7
+ } from "../protected-patterns"
8
+ import {
9
+ buildSubagentResultText,
10
+ getSubAgentId,
11
+ mergeSubagentResult,
12
+ } from "../subagents/subagent-results"
13
+ import { fetchSessionMessages } from "./search"
14
+ import type { SearchContext, SelectionResolution } from "./types"
15
+
16
+ export function appendProtectedUserMessages(
17
+ summary: string,
18
+ selection: SelectionResolution,
19
+ searchContext: SearchContext,
20
+ state: SessionState,
21
+ enabled: boolean,
22
+ ): string {
23
+ if (!enabled) return summary
24
+
25
+ const userTexts: string[] = []
26
+
27
+ for (const messageId of selection.messageIds) {
28
+ const existingCompressionEntry = state.prune.messages.byMessageId.get(messageId)
29
+ if (existingCompressionEntry && existingCompressionEntry.activeBlockIds.length > 0) {
30
+ continue
31
+ }
32
+
33
+ const message = searchContext.rawMessagesById.get(messageId)
34
+ if (!message) continue
35
+ if (message.info.role !== "user") continue
36
+ if (isIgnoredUserMessage(message)) continue
37
+
38
+ const parts = Array.isArray(message.parts) ? message.parts : []
39
+ for (const part of parts) {
40
+ if (part.type === "text" && typeof part.text === "string" && part.text.trim()) {
41
+ userTexts.push(part.text)
42
+ break
43
+ }
44
+ }
45
+ }
46
+
47
+ if (userTexts.length === 0) {
48
+ return summary
49
+ }
50
+
51
+ const heading = "\n\nThe following user messages were sent in this conversation verbatim:"
52
+ const body = userTexts.map((text) => `\n${text}`).join("")
53
+ return summary + heading + body
54
+ }
55
+
56
+ export async function appendProtectedTools(
57
+ client: any,
58
+ state: SessionState,
59
+ allowSubAgents: boolean,
60
+ summary: string,
61
+ selection: SelectionResolution,
62
+ searchContext: SearchContext,
63
+ protectedTools: string[],
64
+ protectedFilePatterns: string[] = [],
65
+ ): Promise<string> {
66
+ const protectedOutputs: string[] = []
67
+
68
+ for (const messageId of selection.messageIds) {
69
+ const existingCompressionEntry = state.prune.messages.byMessageId.get(messageId)
70
+ if (existingCompressionEntry && existingCompressionEntry.activeBlockIds.length > 0) {
71
+ continue
72
+ }
73
+
74
+ const message = searchContext.rawMessagesById.get(messageId)
75
+ if (!message) continue
76
+
77
+ const parts = Array.isArray(message.parts) ? message.parts : []
78
+ for (const part of parts) {
79
+ if (part.type === "tool" && part.callID) {
80
+ let isToolProtected = isToolNameProtected(part.tool, protectedTools)
81
+
82
+ if (!isToolProtected && protectedFilePatterns.length > 0) {
83
+ const filePaths = getFilePathsFromParameters(part.tool, part.state?.input)
84
+ if (isFilePathProtected(filePaths, protectedFilePatterns)) {
85
+ isToolProtected = true
86
+ }
87
+ }
88
+
89
+ if (isToolProtected) {
90
+ const title = `Tool: ${part.tool}`
91
+ let output = ""
92
+
93
+ if (part.state?.status === "completed" && part.state?.output) {
94
+ output =
95
+ typeof part.state.output === "string"
96
+ ? part.state.output
97
+ : JSON.stringify(part.state.output)
98
+ }
99
+
100
+ if (
101
+ allowSubAgents &&
102
+ part.tool === "task" &&
103
+ part.state?.status === "completed" &&
104
+ typeof part.state?.output === "string"
105
+ ) {
106
+ const cachedSubAgentResult = state.subAgentResultCache.get(part.callID)
107
+
108
+ if (cachedSubAgentResult !== undefined) {
109
+ if (cachedSubAgentResult) {
110
+ output = mergeSubagentResult(
111
+ part.state.output,
112
+ cachedSubAgentResult,
113
+ )
114
+ }
115
+ } else {
116
+ const subAgentSessionId = getSubAgentId(part)
117
+ if (subAgentSessionId) {
118
+ let subAgentResultText = ""
119
+ try {
120
+ const subAgentMessages = await fetchSessionMessages(
121
+ client,
122
+ subAgentSessionId,
123
+ )
124
+ subAgentResultText = buildSubagentResultText(subAgentMessages)
125
+ } catch {
126
+ subAgentResultText = ""
127
+ }
128
+
129
+ if (subAgentResultText) {
130
+ state.subAgentResultCache.set(part.callID, subAgentResultText)
131
+ output = mergeSubagentResult(
132
+ part.state.output,
133
+ subAgentResultText,
134
+ )
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ if (output) {
141
+ protectedOutputs.push(`\n### ${title}\n${output}`)
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ if (protectedOutputs.length === 0) {
149
+ return summary
150
+ }
151
+
152
+ const heading = "\n\nThe following protected tools were used in this conversation as well:"
153
+ return summary + heading + protectedOutputs.join("")
154
+ }