@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.
- package/hooks/message-before.ts +138 -1
- package/package.json +2 -2
- package/vectorizer/graph-builder.ts +12 -0
- package/vectorizer/index.ts +14 -1
- package/vectorizer.yaml +1 -0
package/hooks/message-before.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
/**
|
package/vectorizer/index.ts
CHANGED
|
@@ -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
|