@comfanion/usethis_search 4.3.0-dev.3 → 4.3.0-dev.4

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.
@@ -1,21 +1,25 @@
1
1
  /**
2
- * History Pruning Hook (v2no injection)
2
+ * History Pruning Hook (v3workspace-only, DCP handles the rest)
3
3
  *
4
4
  * Uses "experimental.chat.messages.transform" to prune old workspace tool
5
5
  * outputs from chat history. Only the LAST workspace state is kept in context.
6
6
  *
7
- * v2: Removed workspace injection entirely.
8
- * Each tool (search, workspace_list, etc.) now returns full workspace
9
- * state inline. This hook only prunes previous outputs to prevent
10
- * context bloat.
7
+ * v3: Removed read pruning and tool compaction — delegated to DCP plugin
8
+ * (@tarquinen/opencode-dcp) which handles deduplication, supersede-writes,
9
+ * error purging, and agent-facing discard/extract tools.
10
+ *
11
+ * This hook ONLY handles workspace state pruning — something DCP can't do
12
+ * because it doesn't understand that different search queries produce
13
+ * workspace state blocks that supersede each other.
11
14
  *
12
15
  * Pruning strategy:
13
- * 1. WORKSPACE TOOLS: Find all outputs from search/workspace_* tools.
14
- * Keep only the LAST one (it has the latest workspace state).
15
- * Replace the rest with compact 1-line summaries.
16
- * 2. READ TOOLS: Replace old read() outputs with compact summaries.
17
- * Keep the last read output (agent may reference it).
18
- * 3. COMPACT: Remove old tool call parts entirely (keep last N turns).
16
+ * Find all outputs from search/workspace_* tools that contain
17
+ * <workspace_state> blocks. Keep only the LAST one. Replace the rest
18
+ * with compact 1-line summaries.
19
+ *
20
+ * DCP companion config (.opencode/dcp.jsonc):
21
+ * Our workspace tools are added to DCP's protectedTools so DCP
22
+ * doesn't try to prune them (we handle them ourselves).
19
23
  */
20
24
 
21
25
  import type { SessionState } from "./types.ts"
@@ -56,17 +60,11 @@ const WORKSPACE_TOOLS = new Set([
56
60
  /** Minimum output length to consider pruning. Short outputs are kept as-is. */
57
61
  const MIN_PRUNE_LENGTH = 500
58
62
 
59
- /** Keep last N turns intact (don't compact recent tool calls). */
60
- const KEEP_LAST_N_TURNS = 5
61
-
62
- /** Tools eligible for compaction (removing old call + output parts). */
63
- const COMPACT_TOOLS = new Set(["search", "read", "Read", "workspace_list", "workspace_forget", "workspace_clear", "workspace_restore"])
64
-
65
63
  // ── Hook ────────────────────────────────────────────────────────────────────
66
64
 
67
65
  /**
68
66
  * Create the history pruning handler.
69
- * No injection — only prunes old tool outputs from chat history.
67
+ * Only prunes old workspace state outputs DCP handles everything else.
70
68
  */
71
69
  export function createWorkspaceInjectionHandler(state: SessionState) {
72
70
  return async (_input: {}, output: { messages: Message[] }) => {
@@ -74,8 +72,6 @@ export function createWorkspaceInjectionHandler(state: SessionState) {
74
72
  if (state.isSubAgent) return
75
73
 
76
74
  pruneWorkspaceToolOutputs(output.messages)
77
- pruneReadToolOutputs(output.messages)
78
- compactOldToolCalls(output.messages)
79
75
  }
80
76
  }
81
77
 
@@ -84,13 +80,18 @@ export function createWorkspaceInjectionHandler(state: SessionState) {
84
80
  /**
85
81
  * Replace old workspace tool outputs with compact summaries.
86
82
  *
87
- * Workspace tools (search, workspace_list, etc.) now return full workspace
83
+ * Workspace tools (search, workspace_list, etc.) return full workspace
88
84
  * state in their output. Only the LAST such output is kept — all previous
89
85
  * ones are replaced with a 1-line summary.
90
86
  *
91
87
  * This ensures only ONE copy of workspace state is in context at any time.
88
+ *
89
+ * Note: DCP's deduplication only prunes IDENTICAL tool calls (same params).
90
+ * Two different search queries wouldn't be deduplicated by DCP, but both
91
+ * contain workspace state that supersedes each other. That's why we need
92
+ * this workspace-specific pruning.
92
93
  */
93
- function pruneWorkspaceToolOutputs(messages: Message[]): void {
94
+ export function pruneWorkspaceToolOutputs(messages: Message[]): void {
94
95
  const wsParts: { msgIdx: number; partIdx: number; part: MessagePart }[] = []
95
96
 
96
97
  for (let i = 0; i < messages.length; i++) {
@@ -139,174 +140,3 @@ function pruneWorkspaceToolOutputs(messages: Message[]): void {
139
140
  }
140
141
  }
141
142
  }
142
-
143
- // ── Read Tool Pruning ───────────────────────────────────────────────────────
144
-
145
- /**
146
- * Replace old read() tool outputs with compact summaries.
147
- * Keep the last read output (agent may reference it).
148
- */
149
- function pruneReadToolOutputs(messages: Message[]): void {
150
- const readParts: { msgIdx: number; partIdx: number; part: MessagePart }[] = []
151
-
152
- for (let i = 0; i < messages.length; i++) {
153
- const msg = messages[i]
154
- const parts = Array.isArray(msg.parts) ? msg.parts : []
155
-
156
- for (let j = 0; j < parts.length; j++) {
157
- const part = parts[j]
158
- if (
159
- part.type === "tool" &&
160
- (part.tool === "read" || part.tool === "Read") &&
161
- part.state?.status === "completed" &&
162
- typeof part.state?.output === "string" &&
163
- part.state.output.length > MIN_PRUNE_LENGTH
164
- ) {
165
- readParts.push({ msgIdx: i, partIdx: j, part })
166
- }
167
- }
168
- }
169
-
170
- // Keep the last read output — prune the rest
171
- if (readParts.length <= 1) return
172
-
173
- const toPrune = readParts.slice(0, -1)
174
-
175
- for (const { part } of toPrune) {
176
- const output = part.state!.output as string
177
-
178
- // Skip already-pruned outputs
179
- if (output.startsWith("[") || output.startsWith("✓")) continue
180
-
181
- // Extract file path from input or output
182
- const filePath = part.input?.filePath || extractFilePathFromOutput(output)
183
-
184
- part.state!.output = `[Read "${filePath || "file"}" — content pruned from history]`
185
- }
186
- }
187
-
188
- /**
189
- * Extract file path from read() output.
190
- */
191
- function extractFilePathFromOutput(output: string): string | null {
192
- const firstLine = output.split("\n")[0]
193
- const pathMatch = firstLine.match(/##?\s*(.+?\.(ts|js|go|py|md|txt|yaml|json|tsx|jsx|rs|java|kt|swift|c|cpp|h|cs|rb|php))/)
194
- if (pathMatch) {
195
- return pathMatch[1].trim()
196
- }
197
- return null
198
- }
199
-
200
- // ── Tool Call Compaction ────────────────────────────────────────────────────
201
-
202
- /**
203
- * Remove old tool call parts from chat history.
204
- *
205
- * Strategy:
206
- * - Keep last N turns intact
207
- * - Only compact search/read/workspace tools
208
- * - Only compact completed calls with pruned outputs
209
- * - Add compact marker showing how many calls removed
210
- */
211
- function compactOldToolCalls(messages: Message[]): void {
212
- const toolPairs = findToolCallPairs(messages)
213
-
214
- if (toolPairs.length === 0) return
215
-
216
- const totalTurns = messages.length
217
-
218
- // Filter: only old, completed, compactable tools with pruned outputs
219
- const toCompact = toolPairs.filter(pair => {
220
- const turnsFromEnd = totalTurns - pair.msgIndex
221
- return (
222
- turnsFromEnd > KEEP_LAST_N_TURNS &&
223
- pair.status === "completed" &&
224
- COMPACT_TOOLS.has(pair.tool) &&
225
- pair.outputPart &&
226
- isPrunedOutput(pair.outputPart.state?.output || "")
227
- )
228
- })
229
-
230
- if (toCompact.length === 0) return
231
-
232
- // Remove tool parts from messages
233
- const removedIds = new Set<string>()
234
-
235
- for (const pair of toCompact) {
236
- if (pair.callPart.id) removedIds.add(pair.callPart.id)
237
- if (pair.outputPart?.id) removedIds.add(pair.outputPart.id)
238
- }
239
-
240
- for (const msg of messages) {
241
- if (!msg.parts || !Array.isArray(msg.parts)) continue
242
- msg.parts = msg.parts.filter(part => !part.id || !removedIds.has(part.id))
243
- }
244
-
245
- // Add compact marker to first user message
246
- const firstUserMsg = messages.find(m => m?.info?.role === "user")
247
- if (firstUserMsg && firstUserMsg.parts) {
248
- const marker = {
249
- type: "text",
250
- text: `<!-- ${toCompact.length} tool calls compacted (search/read/workspace results pruned) -->`,
251
- id: "compact-marker-" + Date.now(),
252
- }
253
- firstUserMsg.parts.unshift(marker)
254
- }
255
- }
256
-
257
- interface ToolCallPair {
258
- msgIndex: number
259
- callPart: MessagePart
260
- outputPart?: MessagePart
261
- tool: string
262
- status: string
263
- }
264
-
265
- /**
266
- * Find all tool call + output pairs in messages.
267
- */
268
- function findToolCallPairs(messages: Message[]): ToolCallPair[] {
269
- const pairs: ToolCallPair[] = []
270
-
271
- for (let i = 0; i < messages.length; i++) {
272
- const msg = messages[i]
273
- if (!msg.parts || !Array.isArray(msg.parts)) continue
274
-
275
- for (const part of msg.parts) {
276
- if (part.type === "tool" && part.tool) {
277
- const status = part.state?.status || "unknown"
278
-
279
- // Find matching output part (usually in same message)
280
- let outputPart: MessagePart | undefined
281
- for (const p of msg.parts) {
282
- if (p.type === "tool" && p.tool === part.tool && p.state?.output && p.id !== part.id) {
283
- outputPart = p
284
- break
285
- }
286
- }
287
-
288
- pairs.push({
289
- msgIndex: i,
290
- callPart: part,
291
- outputPart,
292
- tool: part.tool,
293
- status,
294
- })
295
- }
296
- }
297
- }
298
-
299
- return pairs
300
- }
301
-
302
- /**
303
- * Check if output is pruned (compact format).
304
- */
305
- function isPrunedOutput(output: string): boolean {
306
- if (!output) return false
307
- return output.startsWith("[") || output.startsWith("✓")
308
- }
309
-
310
- // ── Exports for testing ─────────────────────────────────────────────────────
311
-
312
- export { pruneWorkspaceToolOutputs, pruneReadToolOutputs, compactOldToolCalls }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_search",
3
- "version": "4.3.0-dev.3",
3
+ "version": "4.3.0-dev.4",
4
4
  "description": "OpenCode plugin: semantic search with context-efficient workspace state (v4.3: no injection, each tool returns full state inline, auto-prune history, auto-detect modes, line numbers, LSP memory leak fixed)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",