@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.
- package/LICENSE +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7611 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +25 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +58 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +55 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/system.d.ts +4 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +45 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +102 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +55 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +159 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +458 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +293 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +57 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +305 -0
- package/lib/state/state.ts +210 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +114 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +237 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +401 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +48 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/package.json +91 -5
- package/server.js +1 -0
- 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,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
|
+
}
|