@comfanion/usethis_search 4.3.0-dev.0 → 4.3.0-dev.2

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.
@@ -57,7 +57,9 @@ export function createWorkspaceInjectionHandler(state: SessionState) {
57
57
  // Don't inject or prune for sub-agents (title generation, etc.)
58
58
  if (state.isSubAgent) return
59
59
 
60
- // ── Prune: replace old tool outputs with compact summaries ────────────
60
+ // ── Prune & Compact: optimize chat history ────────────────────────────
61
+ // 1. Prune: replace old tool outputs with compact summaries
62
+ // 2. Compact: remove old tool calls entirely (keep last N turns)
61
63
  // Files are already in workspace injection — no need for big outputs
62
64
  // in chat history. This runs even when workspace is empty
63
65
  // (handles case where workspace was cleared but old outputs remain).
@@ -65,6 +67,7 @@ export function createWorkspaceInjectionHandler(state: SessionState) {
65
67
  if (wsConfig.autoPruneSearch !== false) {
66
68
  pruneSearchToolOutputs(output.messages)
67
69
  pruneReadToolOutputs(output.messages)
70
+ compactOldToolCalls(output.messages)
68
71
  }
69
72
 
70
73
  let entries = workspaceCache.getAll()
@@ -427,3 +430,137 @@ function extractFilePathFromOutput(output: string): string | null {
427
430
 
428
431
  return null
429
432
  }
433
+
434
+ // ── Tool Call Compaction ────────────────────────────────────────────────────
435
+
436
+ /**
437
+ * Remove old tool calls (search/read) from chat history.
438
+ *
439
+ * Strategy:
440
+ * - Keep last N turns (default: 5) — agent may reference recent calls
441
+ * - Only compact search/read tools (not edit/write/grep/glob)
442
+ * - Only compact completed calls with pruned outputs
443
+ * - Remove both call + output parts
444
+ * - Add compact marker at start showing how many calls removed
445
+ *
446
+ * Why: Tool calls contain full args (200+ tokens). After pruning outputs,
447
+ * the calls themselves are redundant — chunks already in workspace.
448
+ *
449
+ * Savings: ~220 tokens per compacted call × N calls = 2K-10K tokens
450
+ */
451
+ const KEEP_LAST_N_TURNS = 5
452
+ const COMPACT_TOOLS = ['search', 'read', 'Read']
453
+
454
+ interface ToolCallPair {
455
+ msgIndex: number
456
+ callPart: MessagePart
457
+ outputPart?: MessagePart
458
+ tool: string
459
+ status: string
460
+ turnsSinceEnd: number
461
+ }
462
+
463
+ /**
464
+ * Compact old tool calls by removing them from chat history.
465
+ * Keeps last N turns intact.
466
+ */
467
+ export function compactOldToolCalls(messages: Message[]): void {
468
+ // Find all tool call pairs
469
+ const toolPairs = findToolCallPairs(messages)
470
+
471
+ if (toolPairs.length === 0) return
472
+
473
+ // Calculate turns from end for each pair
474
+ const totalTurns = messages.length
475
+
476
+ // Filter: only old, completed, search/read with pruned outputs
477
+ const toCompact = toolPairs.filter(pair => {
478
+ const turnsFromEnd = totalTurns - pair.msgIndex
479
+ return (
480
+ turnsFromEnd > KEEP_LAST_N_TURNS &&
481
+ pair.status === 'completed' &&
482
+ COMPACT_TOOLS.includes(pair.tool) &&
483
+ pair.outputPart &&
484
+ isPrunedOutput(pair.outputPart.state?.output || '')
485
+ )
486
+ })
487
+
488
+ if (toCompact.length === 0) return
489
+
490
+ // Remove tool parts from messages
491
+ const removedIds = new Set<string>()
492
+
493
+ for (const pair of toCompact) {
494
+ removedIds.add(pair.callPart.id)
495
+ if (pair.outputPart) {
496
+ removedIds.add(pair.outputPart.id)
497
+ }
498
+ }
499
+
500
+ // Filter out removed parts from messages
501
+ for (const msg of messages) {
502
+ if (!msg.parts || !Array.isArray(msg.parts)) continue
503
+ msg.parts = msg.parts.filter(part => !removedIds.has(part.id))
504
+ }
505
+
506
+ // Add compact marker to first user message
507
+ const firstUserMsg = messages.find(m => m?.info?.role === 'user')
508
+ if (firstUserMsg && firstUserMsg.parts) {
509
+ const marker = {
510
+ type: 'text',
511
+ text: `<!-- ${toCompact.length} tool calls compacted (search/read results in workspace) -->`,
512
+ id: 'compact-marker-' + Date.now(),
513
+ }
514
+ firstUserMsg.parts.unshift(marker)
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Find all tool call + output pairs in messages.
520
+ */
521
+ function findToolCallPairs(messages: Message[]): ToolCallPair[] {
522
+ const pairs: ToolCallPair[] = []
523
+
524
+ for (let i = 0; i < messages.length; i++) {
525
+ const msg = messages[i]
526
+ if (!msg.parts || !Array.isArray(msg.parts)) continue
527
+
528
+ for (const part of msg.parts) {
529
+ if (part.type === 'tool' && part.tool) {
530
+ const status = part.state?.status || 'unknown'
531
+
532
+ // Find matching output part (usually in same message or next)
533
+ let outputPart: MessagePart | undefined
534
+
535
+ // Check same message first
536
+ for (const p of msg.parts) {
537
+ if (p.type === 'tool' && p.tool === part.tool && p.state?.output && p.id !== part.id) {
538
+ outputPart = p
539
+ break
540
+ }
541
+ }
542
+
543
+ pairs.push({
544
+ msgIndex: i,
545
+ callPart: part,
546
+ outputPart,
547
+ tool: part.tool,
548
+ status,
549
+ turnsSinceEnd: 0, // Will be calculated in compactOldToolCalls
550
+ })
551
+ }
552
+ }
553
+ }
554
+
555
+ return pairs
556
+ }
557
+
558
+ /**
559
+ * Check if output is pruned (compact format).
560
+ */
561
+ function isPrunedOutput(output: string): boolean {
562
+ if (!output) return false
563
+
564
+ // Pruned outputs start with [ or ✓
565
+ return output.startsWith('[') || output.startsWith('✓')
566
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comfanion/usethis_search",
3
- "version": "4.3.0-dev.0",
4
- "description": "OpenCode plugin: semantic search with auto-attach, line numbers in workspace, simplified API (v4.3: auto-detect modes, read() caching, 99% token reduction, no grep needed)",
3
+ "version": "4.3.0-dev.2",
4
+ "description": "OpenCode plugin: semantic search with auto-attach, line numbers in workspace, simplified API (v4.3: auto-detect modes, read() caching, tool call compaction, 99% token reduction, no grep needed, LSP memory leak fixed)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
7
7
  "exports": {
@@ -288,6 +288,18 @@ export class GraphBuilder {
288
288
  return result
289
289
  }
290
290
 
291
+ /**
292
+ * Cleanup: shutdown LSP analyzer to prevent memory leaks.
293
+ * MUST be called after indexing to close LSP server processes.
294
+ */
295
+ async cleanup(): Promise<void> {
296
+ try {
297
+ await this.lspAnalyzer.shutdown()
298
+ } catch {
299
+ // Best effort — don't throw if already closed
300
+ }
301
+ }
302
+
291
303
  // ---- FR-005: Semantic similarity edges ------------------------------------
292
304
 
293
305
  /**
@@ -571,11 +571,15 @@ class CodebaseIndexer {
571
571
  try { this.chunkStore.close(); } catch { /* best effort */ }
572
572
  this.chunkStore = null;
573
573
  }
574
+ // Cleanup GraphBuilder (shutdown LSP to prevent memory leaks)
575
+ if (this.graphBuilder) {
576
+ try { await this.graphBuilder.cleanup(); } catch { /* best effort */ }
577
+ this.graphBuilder = null;
578
+ }
574
579
  // Close graph DB to release LevelDB lock
575
580
  if (this.graphDB) {
576
581
  try { await this.graphDB.close(); } catch { /* best effort */ }
577
582
  this.graphDB = null;
578
- this.graphBuilder = null;
579
583
  }
580
584
  // Save & release usage tracker
581
585
  if (this.usageTracker) {
@@ -1623,6 +1627,15 @@ class CodebaseIndexer {
1623
1627
  }
1624
1628
  }
1625
1629
 
1630
+ // Cleanup: shutdown LSP to prevent memory leaks after bulk indexing
1631
+ if (this.graphBuilder) {
1632
+ try {
1633
+ await this.graphBuilder.cleanup();
1634
+ } catch {
1635
+ // Best effort — continue even if cleanup fails
1636
+ }
1637
+ }
1638
+
1626
1639
  return { indexed, skipped, total, semanticEdges };
1627
1640
  }
1628
1641
 
package/vectorizer.yaml CHANGED
@@ -26,6 +26,7 @@ vectorizer:
26
26
  min_chunk_size: 1000 # Merge small sections (avoid header-only chunks)
27
27
  max_chunk_size: 8000 # Large chunks for docs (SQL schemas, API specs, etc.)
28
28
  preserve_heading_hierarchy: true
29
+ skip_low_priority: true # Skip SQL schemas, continuous aggregates (default: true)
29
30
  code:
30
31
  split_by_functions: true
31
32
  include_function_signature: true