lancedb-opencode-pro 0.2.3 → 0.2.5
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/README.md +186 -23
- package/dist/config.js +61 -0
- package/dist/index.js +90 -8
- package/dist/store.d.ts +7 -0
- package/dist/store.js +138 -2
- package/dist/summarize.d.ts +52 -0
- package/dist/summarize.js +350 -0
- package/dist/types.d.ts +56 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -240,6 +240,17 @@ Supported environment variables:
|
|
|
240
240
|
- `LANCEDB_OPENCODE_PRO_UNUSED_DAYS_THRESHOLD`
|
|
241
241
|
- `LANCEDB_OPENCODE_PRO_MIN_CAPTURE_CHARS`
|
|
242
242
|
- `LANCEDB_OPENCODE_PRO_MAX_ENTRIES_PER_SCOPE`
|
|
243
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MODE`
|
|
244
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_MEMORIES`
|
|
245
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MIN_MEMORIES`
|
|
246
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_BUDGET_TOKENS`
|
|
247
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_CHARS_PER_MEMORY`
|
|
248
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARIZATION`
|
|
249
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARY_TARGET_CHARS`
|
|
250
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SCORE_DROP_TOLERANCE`
|
|
251
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_INJECTION_FLOOR`
|
|
252
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_MODE`
|
|
253
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_PRESERVE_STRUCTURE`
|
|
243
254
|
|
|
244
255
|
## What It Provides
|
|
245
256
|
|
|
@@ -357,6 +368,158 @@ Recommended review order in low-feedback environments:
|
|
|
357
368
|
3. Check whether users still needed manual rescue through `memory_search` or issued correction-like responses.
|
|
358
369
|
4. Run a bounded audit of recalled memories or skipped captures before concluding the system is helping.
|
|
359
370
|
|
|
371
|
+
## Injection Control
|
|
372
|
+
|
|
373
|
+
This provider supports configurable memory injection behavior, allowing you to control how recalled memories are processed before being injected into the LLM prompt.
|
|
374
|
+
|
|
375
|
+
### Configuration
|
|
376
|
+
|
|
377
|
+
Add an `injection` block to your sidecar config:
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
{
|
|
381
|
+
"provider": "lancedb-opencode-pro",
|
|
382
|
+
"injection": {
|
|
383
|
+
"mode": "fixed",
|
|
384
|
+
"maxMemories": 3,
|
|
385
|
+
"minMemories": 1,
|
|
386
|
+
"budgetTokens": 4096,
|
|
387
|
+
"maxCharsPerMemory": 1200,
|
|
388
|
+
"summarization": "none",
|
|
389
|
+
"summaryTargetChars": 300,
|
|
390
|
+
"scoreDropTolerance": 0.15,
|
|
391
|
+
"injectionFloor": 0.2,
|
|
392
|
+
"codeSummarization": {
|
|
393
|
+
"mode": "smart",
|
|
394
|
+
"preserveStructure": true
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Injection Modes
|
|
401
|
+
|
|
402
|
+
- **`fixed`** (default) — Always inject up to `maxMemories` memories regardless of content size. This preserves backward-compatible behavior.
|
|
403
|
+
- **`budget`** — Limit total injected tokens to `budgetTokens`. The provider accumulates memories until the token budget is exhausted.
|
|
404
|
+
- **`adaptive`** — Dynamically adjust injection count based on score drops. Stop injection when scores drop below `scoreDropTolerance` relative to the highest-scored memory.
|
|
405
|
+
|
|
406
|
+
### Summarization Modes
|
|
407
|
+
|
|
408
|
+
When `summarization` is set to `truncate` or `extract`, memories are summarized before injection:
|
|
409
|
+
|
|
410
|
+
- **`none`** (default) — No summarization; inject full text.
|
|
411
|
+
- **`truncate`** — Simple truncation to `summaryTargetChars` with ellipsis.
|
|
412
|
+
- **`extract`** — Key sentence extraction for text, structure-preserving truncation for code.
|
|
413
|
+
- **`auto`** — Content-aware summarization (truncate for text, preserve structure for code).
|
|
414
|
+
|
|
415
|
+
### Code Handling
|
|
416
|
+
|
|
417
|
+
The `codeSummarization` config controls how code snippets are processed:
|
|
418
|
+
|
|
419
|
+
- **`mode`**: `"smart"` | `"truncate"` | `"preserve"` (default: `"smart"`)
|
|
420
|
+
- **`preserveStructure`**: When `true`, code truncation attempts to balance brackets and preserve syntactic validity.
|
|
421
|
+
|
|
422
|
+
### Environment Variables
|
|
423
|
+
|
|
424
|
+
All injection options can be overridden via environment variables:
|
|
425
|
+
|
|
426
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MODE`
|
|
427
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_MEMORIES`
|
|
428
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MIN_MEMORIES`
|
|
429
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_BUDGET_TOKENS`
|
|
430
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_MAX_CHARS_PER_MEMORY`
|
|
431
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARIZATION`
|
|
432
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SUMMARY_TARGET_CHARS`
|
|
433
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_SCORE_DROP_TOLERANCE`
|
|
434
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_INJECTION_FLOOR`
|
|
435
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_MODE`
|
|
436
|
+
- `LANCEDB_OPENCODE_PRO_INJECTION_CODE_SUMMARIZATION_PRESERVE_STRUCTURE`
|
|
437
|
+
|
|
438
|
+
### Default Behavior
|
|
439
|
+
|
|
440
|
+
The default configuration preserves backward compatibility:
|
|
441
|
+
|
|
442
|
+
- `mode`: `"fixed"`
|
|
443
|
+
- `maxMemories`: `3`
|
|
444
|
+
- `summarization`: `"none"`
|
|
445
|
+
|
|
446
|
+
This means without any `injection` configuration, the provider behaves identically to previous versions: always inject up to 3 memories with full text.
|
|
447
|
+
|
|
448
|
+
### Example: Token Budget Mode
|
|
449
|
+
|
|
450
|
+
For token-sensitive deployments, use budget mode to limit context size:
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"injection": {
|
|
455
|
+
"mode": "budget",
|
|
456
|
+
"budgetTokens": 1500,
|
|
457
|
+
"summarization": "truncate",
|
|
458
|
+
"summaryTargetChars": 400
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
This configuration:
|
|
464
|
+
1. Accumulates memories until total estimated tokens reach ~1500
|
|
465
|
+
2. Truncates each memory to ~400 characters before injection
|
|
466
|
+
3. Guarantees at least 1 memory is always included
|
|
467
|
+
|
|
468
|
+
### Example: Adaptive Mode
|
|
469
|
+
|
|
470
|
+
For quality-sensitive scenarios where you want to avoid low-relevance memories:
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"injection": {
|
|
475
|
+
"mode": "adaptive",
|
|
476
|
+
"maxMemories": 5,
|
|
477
|
+
"minMemories": 2,
|
|
478
|
+
"scoreDropTolerance": 0.15,
|
|
479
|
+
"injectionFloor": 0.2
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
This configuration:
|
|
485
|
+
1. Starts with up to 5 candidate memories
|
|
486
|
+
2. Stops adding memories when score drops >15% from the top
|
|
487
|
+
3. Ensures minimum score threshold (floor) prevents low-quality injection
|
|
488
|
+
4. Always includes at least 2 memories
|
|
489
|
+
|
|
490
|
+
### Example: Adaptive Mode with Auto Summarization
|
|
491
|
+
|
|
492
|
+
Recommended for users who want intelligent memory injection with content-aware summarization:
|
|
493
|
+
|
|
494
|
+
```json
|
|
495
|
+
{
|
|
496
|
+
"injection": {
|
|
497
|
+
"mode": "adaptive",
|
|
498
|
+
"maxMemories": 5,
|
|
499
|
+
"minMemories": 2,
|
|
500
|
+
"budgetTokens": 4096,
|
|
501
|
+
"maxCharsPerMemory": 1200,
|
|
502
|
+
"summarization": "auto",
|
|
503
|
+
"summaryTargetChars": 400,
|
|
504
|
+
"scoreDropTolerance": 0.15,
|
|
505
|
+
"injectionFloor": 0.2,
|
|
506
|
+
"codeSummarization": {
|
|
507
|
+
"mode": "smart",
|
|
508
|
+
"preserveStructure": true
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
This configuration:
|
|
515
|
+
1. Dynamically adjusts injection count based on relevance scores
|
|
516
|
+
2. Uses content-aware summarization (key sentences for text, smart truncation for code)
|
|
517
|
+
3. Guarantees at least 2 memories are injected
|
|
518
|
+
4. Preserves code structure when truncating
|
|
519
|
+
5. Prevents injection of memories below 0.2 score threshold
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
360
523
|
## OpenAI Embedding Configuration
|
|
361
524
|
|
|
362
525
|
Default behavior stays on Ollama. To use OpenAI embeddings, set `embedding.provider` to `openai` and provide API key + model.
|
|
@@ -500,7 +663,7 @@ The project provides layered validation workflows that can run locally or inside
|
|
|
500
663
|
| `npm run verify` | Typecheck + build + effectiveness workflow + retrieval (quick release check) |
|
|
501
664
|
| `npm run verify:full` | All of the above + benchmark + `npm pack` (full release gate) |
|
|
502
665
|
|
|
503
|
-
Threshold policy and benchmark profiles are documented in `docs/
|
|
666
|
+
Threshold policy and benchmark profiles are documented in `docs/memory-validation-checklist.md` (Phase 4.4).
|
|
504
667
|
Acceptance evidence mapping and archive/ship gate policy are documented in `docs/release-readiness.md`.
|
|
505
668
|
|
|
506
669
|
## Maintainer Release SOP
|
|
@@ -512,7 +675,7 @@ Use this flow when publishing a new version to npm.
|
|
|
512
675
|
|
|
513
676
|
```bash
|
|
514
677
|
docker compose build --no-cache && docker compose up -d
|
|
515
|
-
docker compose exec
|
|
678
|
+
docker compose exec opencode-dev npm run release:check
|
|
516
679
|
```
|
|
517
680
|
|
|
518
681
|
3. Confirm npm authentication:
|
|
@@ -568,8 +731,8 @@ ls -l dist dist-test/src 2>/dev/null
|
|
|
568
731
|
|
|
569
732
|
```bash
|
|
570
733
|
docker compose build --no-cache && docker compose up -d
|
|
571
|
-
docker compose exec
|
|
572
|
-
docker compose exec
|
|
734
|
+
docker compose exec opencode-dev npm run typecheck
|
|
735
|
+
docker compose exec opencode-dev npm run build
|
|
573
736
|
```
|
|
574
737
|
|
|
575
738
|
### Running validation inside Docker
|
|
@@ -578,16 +741,16 @@ docker compose exec app npm run build
|
|
|
578
741
|
docker compose build --no-cache && docker compose up -d
|
|
579
742
|
|
|
580
743
|
# Quick release check
|
|
581
|
-
docker compose exec
|
|
744
|
+
docker compose exec opencode-dev npm run verify
|
|
582
745
|
|
|
583
746
|
# Full release gate (includes benchmark + pack)
|
|
584
|
-
docker compose exec
|
|
747
|
+
docker compose exec opencode-dev npm run verify:full
|
|
585
748
|
|
|
586
749
|
# Individual workflows
|
|
587
|
-
docker compose exec
|
|
588
|
-
docker compose exec
|
|
589
|
-
docker compose exec
|
|
590
|
-
docker compose exec
|
|
750
|
+
docker compose exec opencode-dev npm run test:foundation
|
|
751
|
+
docker compose exec opencode-dev npm run test:regression
|
|
752
|
+
docker compose exec opencode-dev npm run test:retrieval
|
|
753
|
+
docker compose exec opencode-dev npm run benchmark:latency
|
|
591
754
|
```
|
|
592
755
|
|
|
593
756
|
### Operator verification
|
|
@@ -596,15 +759,15 @@ After running `npm run verify:full`, operators can inspect the following:
|
|
|
596
759
|
|
|
597
760
|
```bash
|
|
598
761
|
# Confirm the packaged build is installable
|
|
599
|
-
docker compose exec
|
|
762
|
+
docker compose exec opencode-dev ls -la lancedb-opencode-pro-*.tgz
|
|
600
763
|
|
|
601
764
|
# Confirm typecheck and build succeeded
|
|
602
|
-
docker compose exec
|
|
603
|
-
docker compose exec
|
|
765
|
+
docker compose exec opencode-dev npm run typecheck
|
|
766
|
+
docker compose exec opencode-dev npm run build
|
|
604
767
|
|
|
605
768
|
# Check resolved default storage path
|
|
606
|
-
docker compose exec
|
|
607
|
-
docker compose exec
|
|
769
|
+
docker compose exec opencode-dev node -e "import('./dist/index.js').then(() => console.log('plugin loaded'))"
|
|
770
|
+
docker compose exec opencode-dev sh -lc 'ls -la ~/.opencode/memory/lancedb 2>/dev/null || echo "No data yet (expected before first use)"'
|
|
608
771
|
```
|
|
609
772
|
|
|
610
773
|
## Long Memory Verification
|
|
@@ -622,14 +785,14 @@ docker compose build --no-cache && docker compose up -d
|
|
|
622
785
|
The E2E script loads `dist/index.js`, so build artifacts must exist first.
|
|
623
786
|
|
|
624
787
|
```bash
|
|
625
|
-
docker compose exec
|
|
626
|
-
docker compose exec
|
|
788
|
+
docker compose exec opencode-dev npm install
|
|
789
|
+
docker compose exec opencode-dev npm run build
|
|
627
790
|
```
|
|
628
791
|
|
|
629
792
|
### 3. Run the built-in end-to-end memory test
|
|
630
793
|
|
|
631
794
|
```bash
|
|
632
|
-
docker compose exec
|
|
795
|
+
docker compose exec opencode-dev npm run test:e2e
|
|
633
796
|
```
|
|
634
797
|
|
|
635
798
|
Expected success output:
|
|
@@ -651,7 +814,7 @@ This verifies all of the following in one run:
|
|
|
651
814
|
The E2E script uses `/tmp/opencode-memory-e2e` as its test database path.
|
|
652
815
|
|
|
653
816
|
```bash
|
|
654
|
-
docker compose exec
|
|
817
|
+
docker compose exec opencode-dev ls -la /tmp/opencode-memory-e2e
|
|
655
818
|
```
|
|
656
819
|
|
|
657
820
|
If files appear in that directory after the E2E run, memory was written to disk instead of only being kept in process memory.
|
|
@@ -667,7 +830,7 @@ When running through the normal plugin config, the default durable storage path
|
|
|
667
830
|
Check it inside the container with:
|
|
668
831
|
|
|
669
832
|
```bash
|
|
670
|
-
docker compose exec
|
|
833
|
+
docker compose exec opencode-dev sh -lc 'ls -la ~/.opencode/memory/lancedb'
|
|
671
834
|
```
|
|
672
835
|
|
|
673
836
|
### 6. Stronger proof: verify retrieval still works after restart
|
|
@@ -676,8 +839,8 @@ Long memory is only convincing if retrieval still works after the runtime is res
|
|
|
676
839
|
|
|
677
840
|
```bash
|
|
678
841
|
docker compose restart app
|
|
679
|
-
docker compose exec
|
|
680
|
-
docker compose exec
|
|
842
|
+
docker compose exec opencode-dev npm run test:e2e
|
|
843
|
+
docker compose exec opencode-dev ls -la /tmp/opencode-memory-e2e
|
|
681
844
|
```
|
|
682
845
|
|
|
683
846
|
If the search step still succeeds after restart and the database files remain present, that is strong evidence that the memory is durable.
|
|
@@ -686,7 +849,7 @@ If the search step still succeeds after restart and the database files remain pr
|
|
|
686
849
|
|
|
687
850
|
Treat the feature as verified only when all of these are true:
|
|
688
851
|
|
|
689
|
-
- `docker compose exec
|
|
852
|
+
- `docker compose exec opencode-dev npm run test:e2e` passes
|
|
690
853
|
- `/tmp/opencode-memory-e2e` contains LanceDB files after the run
|
|
691
854
|
- the memory retrieval step still succeeds after container restart
|
|
692
855
|
- the configured OpenCode storage path exists when running real plugin integration
|
package/dist/config.js
CHANGED
|
@@ -38,6 +38,8 @@ export function resolveMemoryConfig(config, worktree) {
|
|
|
38
38
|
? process.env.LANCEDB_OPENCODE_PRO_OPENAI_TIMEOUT_MS ?? process.env.LANCEDB_OPENCODE_PRO_EMBEDDING_TIMEOUT_MS
|
|
39
39
|
: process.env.LANCEDB_OPENCODE_PRO_EMBEDDING_TIMEOUT_MS;
|
|
40
40
|
const timeoutRaw = timeoutEnv ?? embeddingRaw.timeoutMs;
|
|
41
|
+
const injection = resolveInjectionConfig(raw, process.env);
|
|
42
|
+
const dedup = resolveDedupConfig(raw, process.env);
|
|
41
43
|
const resolvedConfig = {
|
|
42
44
|
provider,
|
|
43
45
|
dbPath,
|
|
@@ -58,6 +60,8 @@ export function resolveMemoryConfig(config, worktree) {
|
|
|
58
60
|
recencyHalfLifeHours,
|
|
59
61
|
importanceWeight,
|
|
60
62
|
},
|
|
63
|
+
injection,
|
|
64
|
+
dedup,
|
|
61
65
|
includeGlobalScope: toBoolean(process.env.LANCEDB_OPENCODE_PRO_INCLUDE_GLOBAL_SCOPE ?? raw.includeGlobalScope, true),
|
|
62
66
|
globalDetectionThreshold: Math.max(1, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DETECTION_THRESHOLD ?? raw.globalDetectionThreshold, 2))),
|
|
63
67
|
globalDiscountFactor: clamp(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DISCOUNT_FACTOR ?? raw.globalDiscountFactor, 0.7), 0, 1),
|
|
@@ -75,6 +79,51 @@ function resolveEmbeddingProvider(raw) {
|
|
|
75
79
|
return "openai";
|
|
76
80
|
throw new Error(`[lancedb-opencode-pro] Invalid embedding provider "${raw}". Expected "ollama" or "openai".`);
|
|
77
81
|
}
|
|
82
|
+
function resolveInjectionMode(raw) {
|
|
83
|
+
if (raw === "fixed" || raw === "budget" || raw === "adaptive")
|
|
84
|
+
return raw;
|
|
85
|
+
return "fixed";
|
|
86
|
+
}
|
|
87
|
+
function resolveSummarizationMode(raw) {
|
|
88
|
+
if (raw === "none" || raw === "truncate" || raw === "extract" || raw === "auto")
|
|
89
|
+
return raw;
|
|
90
|
+
return "none";
|
|
91
|
+
}
|
|
92
|
+
function resolveCodeTruncationMode(raw) {
|
|
93
|
+
if (raw === "smart" || raw === "signature" || raw === "preserve")
|
|
94
|
+
return raw;
|
|
95
|
+
return "smart";
|
|
96
|
+
}
|
|
97
|
+
function resolveDedupConfig(raw, env) {
|
|
98
|
+
const dedupRaw = (raw.dedup ?? {});
|
|
99
|
+
const enabled = toBoolean(env.LANCEDB_OPENCODE_PRO_DEDUP_ENABLED ?? dedupRaw.enabled, true);
|
|
100
|
+
const writeThreshold = clamp(toNumber(env.LANCEDB_OPENCODE_PRO_DEDUP_WRITE_THRESHOLD ?? dedupRaw.writeThreshold, 0.92), 0.0, 1.0);
|
|
101
|
+
const consolidateThreshold = clamp(toNumber(env.LANCEDB_OPENCODE_PRO_DEDUP_CONSOLIDATE_THRESHOLD ?? dedupRaw.consolidateThreshold, 0.95), 0.0, 1.0);
|
|
102
|
+
return { enabled, writeThreshold, consolidateThreshold };
|
|
103
|
+
}
|
|
104
|
+
function resolveInjectionConfig(raw, env) {
|
|
105
|
+
const injectionRaw = (raw.injection ?? {});
|
|
106
|
+
const codeSummarizationRaw = (injectionRaw.codeSummarization ?? {});
|
|
107
|
+
return {
|
|
108
|
+
mode: resolveInjectionMode(env.LANCEDB_OPENCODE_PRO_INJECTION_MODE ?? injectionRaw.mode),
|
|
109
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MAX_MEMORIES ?? injectionRaw.maxMemories, 3))),
|
|
110
|
+
minMemories: Math.max(1, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MIN_MEMORIES ?? injectionRaw.minMemories, 1))),
|
|
111
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_BUDGET_TOKENS ?? injectionRaw.budgetTokens, 4096))),
|
|
112
|
+
maxCharsPerMemory: Math.max(100, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MAX_CHARS ?? injectionRaw.maxCharsPerMemory, 1200))),
|
|
113
|
+
summarization: resolveSummarizationMode(env.LANCEDB_OPENCODE_PRO_INJECTION_SUMMARIZATION ?? injectionRaw.summarization),
|
|
114
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_SUMMARY_TARGET_CHARS ?? injectionRaw.summaryTargetChars, 300))),
|
|
115
|
+
scoreDropTolerance: clamp(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_SCORE_DROP_TOLERANCE ?? injectionRaw.scoreDropTolerance, 0.15), 0, 1),
|
|
116
|
+
injectionFloor: clamp(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_FLOOR ?? injectionRaw.injectionFloor, 0.2), 0, 1),
|
|
117
|
+
codeSummarization: {
|
|
118
|
+
enabled: toBoolean(env.LANCEDB_OPENCODE_PRO_CODE_SUMMARIZATION_ENABLED ?? codeSummarizationRaw.enabled, true),
|
|
119
|
+
pureCodeThreshold: Math.max(100, Math.floor(toNumber(codeSummarizationRaw.pureCodeThreshold, 500))),
|
|
120
|
+
maxCodeLines: Math.max(5, Math.floor(toNumber(codeSummarizationRaw.maxCodeLines, 15))),
|
|
121
|
+
codeTruncationMode: resolveCodeTruncationMode(codeSummarizationRaw.codeTruncationMode),
|
|
122
|
+
preserveComments: toBoolean(codeSummarizationRaw.preserveComments, true),
|
|
123
|
+
preserveImports: toBoolean(codeSummarizationRaw.preserveImports, false),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
78
127
|
function validateEmbeddingConfig(embedding) {
|
|
79
128
|
if (embedding.provider !== "openai")
|
|
80
129
|
return;
|
|
@@ -130,6 +179,18 @@ function mergeMemoryConfig(base, override) {
|
|
|
130
179
|
...(base.retrieval ?? {}),
|
|
131
180
|
...(override.retrieval ?? {}),
|
|
132
181
|
},
|
|
182
|
+
injection: {
|
|
183
|
+
...(base.injection ?? {}),
|
|
184
|
+
...(override.injection ?? {}),
|
|
185
|
+
codeSummarization: {
|
|
186
|
+
...((base.injection ?? {}).codeSummarization ?? {}),
|
|
187
|
+
...((override.injection ?? {}).codeSummarization ?? {}),
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
dedup: {
|
|
191
|
+
...(base.dedup ?? {}),
|
|
192
|
+
...(override.dedup ?? {}),
|
|
193
|
+
},
|
|
133
194
|
};
|
|
134
195
|
}
|
|
135
196
|
function firstString(...values) {
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { isTcpPortAvailable, parsePortReservations, planPorts, reservationKey }
|
|
|
6
6
|
import { buildScopeFilter, deriveProjectScope } from "./scope.js";
|
|
7
7
|
import { MemoryStore } from "./store.js";
|
|
8
8
|
import { generateId } from "./utils.js";
|
|
9
|
+
import { calculateInjectionLimit, createSummarizationConfig, summarizeContent } from "./summarize.js";
|
|
9
10
|
const SCHEMA_VERSION = 1;
|
|
10
11
|
const plugin = async (input) => {
|
|
11
12
|
const state = await createRuntimeState(input);
|
|
@@ -22,6 +23,10 @@ const plugin = async (input) => {
|
|
|
22
23
|
if (event.type === "session.idle" || event.type === "session.compacted") {
|
|
23
24
|
const sessionID = event.properties.sessionID;
|
|
24
25
|
await flushAutoCapture(sessionID, state, input.client);
|
|
26
|
+
if (event.type === "session.compacted" && state.config.dedup.enabled) {
|
|
27
|
+
const activeScope = deriveProjectScope(input.worktree);
|
|
28
|
+
state.store.consolidateDuplicates(activeScope, state.config.dedup.consolidateThreshold).catch(() => { });
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
},
|
|
27
32
|
"experimental.text.complete": async (eventInput, eventOutput) => {
|
|
@@ -52,16 +57,19 @@ const plugin = async (input) => {
|
|
|
52
57
|
query,
|
|
53
58
|
queryVector,
|
|
54
59
|
scopes,
|
|
55
|
-
limit:
|
|
60
|
+
limit: state.config.injection.maxMemories * 2, // Fetch more than needed for filtering
|
|
56
61
|
vectorWeight: state.config.retrieval.mode === "vector" ? 1 : state.config.retrieval.vectorWeight,
|
|
57
62
|
bm25Weight: state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight,
|
|
58
|
-
minScore: state.config.retrieval.minScore,
|
|
63
|
+
minScore: Math.max(state.config.retrieval.minScore, state.config.injection.injectionFloor),
|
|
59
64
|
rrfK: state.config.retrieval.rrfK,
|
|
60
65
|
recencyBoost: state.config.retrieval.recencyBoost,
|
|
61
66
|
recencyHalfLifeHours: state.config.retrieval.recencyHalfLifeHours,
|
|
62
67
|
importanceWeight: state.config.retrieval.importanceWeight,
|
|
63
68
|
globalDiscountFactor: state.config.globalDiscountFactor,
|
|
64
69
|
});
|
|
70
|
+
// Apply injection control
|
|
71
|
+
const injectionLimit = calculateInjectionLimit(results, state.config.injection);
|
|
72
|
+
const limitedResults = results.slice(0, injectionLimit);
|
|
65
73
|
await state.store.putEvent({
|
|
66
74
|
id: generateId(),
|
|
67
75
|
type: "recall",
|
|
@@ -69,21 +77,32 @@ const plugin = async (input) => {
|
|
|
69
77
|
scope: activeScope,
|
|
70
78
|
sessionID: eventInput.sessionID,
|
|
71
79
|
timestamp: Date.now(),
|
|
72
|
-
resultCount:
|
|
73
|
-
injected:
|
|
80
|
+
resultCount: limitedResults.length,
|
|
81
|
+
injected: limitedResults.length > 0,
|
|
74
82
|
metadataJson: JSON.stringify({
|
|
75
83
|
source: "system-transform",
|
|
76
84
|
includeGlobalScope: state.config.includeGlobalScope,
|
|
85
|
+
injectionMode: state.config.injection.mode,
|
|
86
|
+
injectionLimit: injectionLimit,
|
|
77
87
|
}),
|
|
78
88
|
});
|
|
79
|
-
if (
|
|
89
|
+
if (limitedResults.length === 0)
|
|
80
90
|
return;
|
|
81
|
-
for (const result of
|
|
91
|
+
for (const result of limitedResults) {
|
|
82
92
|
state.store.updateMemoryUsage(result.record.id, activeScope, scopes).catch(() => { });
|
|
83
93
|
}
|
|
94
|
+
// Apply summarization if configured
|
|
95
|
+
const summarizationConfig = createSummarizationConfig(state.config.injection);
|
|
96
|
+
const processedResults = limitedResults.map((item) => {
|
|
97
|
+
if (state.config.injection.summarization === "none") {
|
|
98
|
+
return { ...item, text: item.record.text };
|
|
99
|
+
}
|
|
100
|
+
const summarized = summarizeContent(item.record.text, summarizationConfig);
|
|
101
|
+
return { ...item, text: summarized.content };
|
|
102
|
+
});
|
|
84
103
|
const memoryBlock = [
|
|
85
104
|
"[Memory Recall - optional historical context]",
|
|
86
|
-
...
|
|
105
|
+
...processedResults.map((item, index) => `${index + 1}. [${item.record.id}] (${item.record.scope}) ${item.text}`),
|
|
87
106
|
"Use these as optional hints only; prioritize current user intent and current repo state.",
|
|
88
107
|
].join("\n");
|
|
89
108
|
eventOutput.system.push(memoryBlock);
|
|
@@ -142,7 +161,9 @@ const plugin = async (input) => {
|
|
|
142
161
|
return results
|
|
143
162
|
.map((item, idx) => {
|
|
144
163
|
const percent = Math.round(item.score * 100);
|
|
145
|
-
|
|
164
|
+
const meta = JSON.parse(item.record.metadataJson || "{}");
|
|
165
|
+
const duplicateMarker = meta.isPotentialDuplicate ? " (duplicate)" : "";
|
|
166
|
+
return `${idx + 1}. [${item.record.id}]${duplicateMarker} (${item.record.scope}) ${item.record.text} [${percent}%]`;
|
|
146
167
|
})
|
|
147
168
|
.join("\n");
|
|
148
169
|
},
|
|
@@ -414,6 +435,45 @@ const plugin = async (input) => {
|
|
|
414
435
|
.join("\n");
|
|
415
436
|
},
|
|
416
437
|
}),
|
|
438
|
+
memory_consolidate: tool({
|
|
439
|
+
description: "Scope-internally merge near-duplicate memories. Use to clean up accumulated duplicates.",
|
|
440
|
+
args: {
|
|
441
|
+
scope: tool.schema.string().optional(),
|
|
442
|
+
confirm: tool.schema.boolean().default(false),
|
|
443
|
+
},
|
|
444
|
+
execute: async (args, context) => {
|
|
445
|
+
await state.ensureInitialized();
|
|
446
|
+
if (!state.initialized)
|
|
447
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
448
|
+
if (!args.confirm) {
|
|
449
|
+
return "Rejected: memory_consolidate requires confirm=true.";
|
|
450
|
+
}
|
|
451
|
+
const targetScope = args.scope ?? deriveProjectScope(context.worktree);
|
|
452
|
+
const result = await state.store.consolidateDuplicates(targetScope, state.config.dedup.consolidateThreshold);
|
|
453
|
+
return JSON.stringify({ scope: targetScope, ...result }, null, 2);
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
456
|
+
memory_consolidate_all: tool({
|
|
457
|
+
description: "Consolidate duplicates across global scope and current project scope. Used by external cron jobs for daily cleanup.",
|
|
458
|
+
args: {
|
|
459
|
+
confirm: tool.schema.boolean().default(false),
|
|
460
|
+
},
|
|
461
|
+
execute: async (args, context) => {
|
|
462
|
+
await state.ensureInitialized();
|
|
463
|
+
if (!state.initialized)
|
|
464
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
465
|
+
if (!args.confirm) {
|
|
466
|
+
return "Rejected: memory_consolidate_all requires confirm=true.";
|
|
467
|
+
}
|
|
468
|
+
const projectScope = deriveProjectScope(context.worktree);
|
|
469
|
+
const globalResult = await state.store.consolidateDuplicates("global", state.config.dedup.consolidateThreshold);
|
|
470
|
+
const projectResult = await state.store.consolidateDuplicates(projectScope, state.config.dedup.consolidateThreshold);
|
|
471
|
+
return JSON.stringify({
|
|
472
|
+
global: { scope: "global", ...globalResult },
|
|
473
|
+
project: { scope: projectScope, ...projectResult },
|
|
474
|
+
}, null, 2);
|
|
475
|
+
},
|
|
476
|
+
}),
|
|
417
477
|
memory_port_plan: tool({
|
|
418
478
|
description: "Plan non-conflicting host ports for compose services and optionally persist reservations",
|
|
419
479
|
args: {
|
|
@@ -623,6 +683,26 @@ async function flushAutoCapture(sessionID, state, client) {
|
|
|
623
683
|
});
|
|
624
684
|
return;
|
|
625
685
|
}
|
|
686
|
+
let isPotentialDuplicate = false;
|
|
687
|
+
let duplicateOf = null;
|
|
688
|
+
if (state.config.dedup.enabled) {
|
|
689
|
+
const similar = await state.store.search({
|
|
690
|
+
query: result.candidate.text,
|
|
691
|
+
queryVector: vector,
|
|
692
|
+
scopes: [activeScope],
|
|
693
|
+
limit: 1,
|
|
694
|
+
vectorWeight: 1.0,
|
|
695
|
+
bm25Weight: 0.0,
|
|
696
|
+
minScore: 0.0,
|
|
697
|
+
rrfK: 60,
|
|
698
|
+
recencyBoost: false,
|
|
699
|
+
globalDiscountFactor: 1.0,
|
|
700
|
+
});
|
|
701
|
+
if (similar.length > 0 && similar[0].score >= state.config.dedup.writeThreshold) {
|
|
702
|
+
isPotentialDuplicate = true;
|
|
703
|
+
duplicateOf = similar[0].record.id;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
626
706
|
const memoryId = generateId();
|
|
627
707
|
await state.store.put({
|
|
628
708
|
id: memoryId,
|
|
@@ -641,6 +721,8 @@ async function flushAutoCapture(sessionID, state, client) {
|
|
|
641
721
|
metadataJson: JSON.stringify({
|
|
642
722
|
source: "auto-capture",
|
|
643
723
|
sessionID,
|
|
724
|
+
isPotentialDuplicate,
|
|
725
|
+
duplicateOf,
|
|
644
726
|
}),
|
|
645
727
|
});
|
|
646
728
|
await recordCaptureEvent(state, {
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EffectivenessSummary, MemoryEffectivenessEvent, MemoryRecord, SearchResult } from "./types.js";
|
|
2
|
+
export declare function storeFastCosine(a: number[], b: number[], normA: number, normB: number): number;
|
|
2
3
|
export declare class MemoryStore {
|
|
3
4
|
private readonly dbPath;
|
|
4
5
|
private lancedb;
|
|
@@ -32,6 +33,11 @@ export declare class MemoryStore {
|
|
|
32
33
|
clearScope(scope: string): Promise<number>;
|
|
33
34
|
list(scope: string, limit: number): Promise<MemoryRecord[]>;
|
|
34
35
|
pruneScope(scope: string, maxEntries: number): Promise<number>;
|
|
36
|
+
consolidateDuplicates(scope: string, threshold: number): Promise<{
|
|
37
|
+
mergedPairs: number;
|
|
38
|
+
updatedRecords: number;
|
|
39
|
+
skippedRecords: number;
|
|
40
|
+
}>;
|
|
35
41
|
countIncompatibleVectors(scopes: string[], expectedDim: number): Promise<number>;
|
|
36
42
|
private matchesId;
|
|
37
43
|
hasMemory(id: string, scopes: string[]): Promise<boolean>;
|
|
@@ -48,6 +54,7 @@ export declare class MemoryStore {
|
|
|
48
54
|
private requireTable;
|
|
49
55
|
private requireEventTable;
|
|
50
56
|
private readEventsByScopes;
|
|
57
|
+
private readByScopesIncludingMerged;
|
|
51
58
|
private readByScopes;
|
|
52
59
|
private ensureIndexes;
|
|
53
60
|
private ensureMemoriesTableCompatibility;
|
package/dist/store.js
CHANGED
|
@@ -4,6 +4,19 @@ import { tokenize } from "./utils.js";
|
|
|
4
4
|
const TABLE_NAME = "memories";
|
|
5
5
|
const EVENTS_TABLE_NAME = "effectiveness_events";
|
|
6
6
|
const EVENTS_SOURCE_COLUMN = "source";
|
|
7
|
+
// Exported for use by consolidateDuplicates
|
|
8
|
+
export function storeFastCosine(a, b, normA, normB) {
|
|
9
|
+
if (a.length === 0 || b.length === 0 || a.length !== b.length)
|
|
10
|
+
return 0;
|
|
11
|
+
const denom = normA * normB;
|
|
12
|
+
if (denom === 0)
|
|
13
|
+
return 0;
|
|
14
|
+
let dot = 0;
|
|
15
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
16
|
+
dot += a[i] * b[i];
|
|
17
|
+
}
|
|
18
|
+
return dot / denom;
|
|
19
|
+
}
|
|
7
20
|
export class MemoryStore {
|
|
8
21
|
dbPath;
|
|
9
22
|
lancedb = null;
|
|
@@ -209,13 +222,83 @@ export class MemoryStore {
|
|
|
209
222
|
const rows = await this.list(scope, 100000);
|
|
210
223
|
if (rows.length <= maxEntries)
|
|
211
224
|
return 0;
|
|
212
|
-
const
|
|
225
|
+
const flagged = rows.filter((r) => {
|
|
226
|
+
const meta = parseMetadata(r.metadataJson);
|
|
227
|
+
return meta.isPotentialDuplicate === true;
|
|
228
|
+
});
|
|
229
|
+
const unflagged = rows.filter((r) => {
|
|
230
|
+
const meta = parseMetadata(r.metadataJson);
|
|
231
|
+
return meta.isPotentialDuplicate !== true;
|
|
232
|
+
});
|
|
233
|
+
const sortedFlagged = flagged.sort((a, b) => a.timestamp - b.timestamp);
|
|
234
|
+
const sortedUnflagged = unflagged.sort((a, b) => a.timestamp - b.timestamp);
|
|
235
|
+
const toDeleteCount = rows.length - maxEntries;
|
|
236
|
+
const deleteFromFlagged = Math.min(sortedFlagged.length, toDeleteCount);
|
|
237
|
+
const toDelete = [
|
|
238
|
+
...sortedFlagged.slice(0, deleteFromFlagged),
|
|
239
|
+
...sortedUnflagged.slice(0, toDeleteCount - deleteFromFlagged),
|
|
240
|
+
];
|
|
213
241
|
for (const row of toDelete) {
|
|
214
242
|
await this.requireTable().delete(`id = '${escapeSql(row.id)}'`);
|
|
215
243
|
}
|
|
216
244
|
this.invalidateScope(scope);
|
|
217
245
|
return toDelete.length;
|
|
218
246
|
}
|
|
247
|
+
async consolidateDuplicates(scope, threshold) {
|
|
248
|
+
const rows = await this.readByScopesIncludingMerged([scope]);
|
|
249
|
+
if (rows.length === 0) {
|
|
250
|
+
return { mergedPairs: 0, updatedRecords: 0, skippedRecords: 0 };
|
|
251
|
+
}
|
|
252
|
+
let mergedPairs = 0;
|
|
253
|
+
let updatedRecords = 0;
|
|
254
|
+
let skippedRecords = 0;
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
257
|
+
const rowsWithNorms = rows.map((row) => ({
|
|
258
|
+
row,
|
|
259
|
+
norm: this.scopeCache.get(scope)?.norms.get(row.id) ?? vecNorm(row.vector),
|
|
260
|
+
}));
|
|
261
|
+
for (let i = 0; i < rowsWithNorms.length; i += 1) {
|
|
262
|
+
const a = rowsWithNorms[i];
|
|
263
|
+
for (let j = i + 1; j < rowsWithNorms.length; j += 1) {
|
|
264
|
+
const b = rowsWithNorms[j];
|
|
265
|
+
const sim = storeFastCosine(a.row.vector, b.row.vector, a.norm, b.norm);
|
|
266
|
+
if (sim < threshold)
|
|
267
|
+
continue;
|
|
268
|
+
const aMeta = parseMetadata(a.row.metadataJson);
|
|
269
|
+
if (aMeta.status === "merged") {
|
|
270
|
+
skippedRecords += 1;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (a.row.lastRecalled > 0 && now - a.row.lastRecalled < FIVE_MINUTES_MS) {
|
|
274
|
+
skippedRecords += 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const older = a.row.timestamp <= b.row.timestamp ? a.row : b.row;
|
|
278
|
+
const newer = a.row.timestamp <= b.row.timestamp ? b.row : a.row;
|
|
279
|
+
const newerMeta = parseMetadata(newer.metadataJson);
|
|
280
|
+
const mergedIntoId = newer.id;
|
|
281
|
+
const updatedOlderMeta = { status: "merged", mergedInto: mergedIntoId };
|
|
282
|
+
await this.requireTable().delete(`id = '${escapeSql(older.id)}'`);
|
|
283
|
+
await this.requireTable().add([{
|
|
284
|
+
...older,
|
|
285
|
+
metadataJson: JSON.stringify({ ...parseMetadata(older.metadataJson), ...updatedOlderMeta }),
|
|
286
|
+
}]);
|
|
287
|
+
const updatedNewerMeta = { ...newerMeta, mergedFrom: older.id };
|
|
288
|
+
await this.requireTable().delete(`id = '${escapeSql(newer.id)}'`);
|
|
289
|
+
await this.requireTable().add([{
|
|
290
|
+
...newer,
|
|
291
|
+
metadataJson: JSON.stringify(updatedNewerMeta),
|
|
292
|
+
}]);
|
|
293
|
+
mergedPairs += 1;
|
|
294
|
+
updatedRecords += 2;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (mergedPairs > 0) {
|
|
298
|
+
this.invalidateScope(scope);
|
|
299
|
+
}
|
|
300
|
+
return { mergedPairs, updatedRecords, skippedRecords };
|
|
301
|
+
}
|
|
219
302
|
async countIncompatibleVectors(scopes, expectedDim) {
|
|
220
303
|
const rows = await this.readByScopes(scopes);
|
|
221
304
|
return rows.filter((row) => row.vectorDim !== expectedDim).length;
|
|
@@ -279,6 +362,8 @@ export class MemoryStore {
|
|
|
279
362
|
async summarizeEvents(scope, includeGlobalScope) {
|
|
280
363
|
const scopes = includeGlobalScope && scope !== "global" ? [scope, "global"] : [scope];
|
|
281
364
|
const events = await this.readEventsByScopes(scopes);
|
|
365
|
+
// Read all memories including merged for duplicate counts
|
|
366
|
+
const memories = await this.readByScopesIncludingMerged(scopes);
|
|
282
367
|
const captureSkipReasons = {};
|
|
283
368
|
let captureConsidered = 0;
|
|
284
369
|
let captureStored = 0;
|
|
@@ -343,6 +428,15 @@ export class MemoryStore {
|
|
|
343
428
|
}
|
|
344
429
|
const totalCaptureAttempts = captureStored + captureSkipped;
|
|
345
430
|
const totalUsefulFeedback = feedbackUsefulPositive + feedbackUsefulNegative;
|
|
431
|
+
// Count flagged (isPotentialDuplicate) and consolidated (status=merged) from memories table
|
|
432
|
+
const flaggedCount = memories.filter((r) => {
|
|
433
|
+
const meta = parseMetadata(r.metadataJson);
|
|
434
|
+
return meta.isPotentialDuplicate === true;
|
|
435
|
+
}).length;
|
|
436
|
+
const consolidatedCount = memories.filter((r) => {
|
|
437
|
+
const meta = parseMetadata(r.metadataJson);
|
|
438
|
+
return meta.status === "merged";
|
|
439
|
+
}).length;
|
|
346
440
|
return {
|
|
347
441
|
scope,
|
|
348
442
|
totalEvents: events.length,
|
|
@@ -384,6 +478,10 @@ export class MemoryStore {
|
|
|
384
478
|
falsePositiveRate: captureStored === 0 ? 0 : feedbackWrong / captureStored,
|
|
385
479
|
falseNegativeRate: totalCaptureAttempts === 0 ? 0 : feedbackMissing / totalCaptureAttempts,
|
|
386
480
|
},
|
|
481
|
+
duplicates: {
|
|
482
|
+
flaggedCount,
|
|
483
|
+
consolidatedCount,
|
|
484
|
+
},
|
|
387
485
|
};
|
|
388
486
|
}
|
|
389
487
|
getIndexHealth() {
|
|
@@ -469,7 +567,7 @@ export class MemoryStore {
|
|
|
469
567
|
.map((row) => normalizeEventRow(row))
|
|
470
568
|
.filter((row) => row !== null);
|
|
471
569
|
}
|
|
472
|
-
async
|
|
570
|
+
async readByScopesIncludingMerged(scopes) {
|
|
473
571
|
const table = this.requireTable();
|
|
474
572
|
if (scopes.length === 0)
|
|
475
573
|
return [];
|
|
@@ -499,6 +597,36 @@ export class MemoryStore {
|
|
|
499
597
|
.map((row) => normalizeRow(row))
|
|
500
598
|
.filter((row) => row !== null);
|
|
501
599
|
}
|
|
600
|
+
async readByScopes(scopes) {
|
|
601
|
+
const table = this.requireTable();
|
|
602
|
+
if (scopes.length === 0)
|
|
603
|
+
return [];
|
|
604
|
+
const whereExpr = scopes.map((scope) => `scope = '${escapeSql(scope)}'`).join(" OR ");
|
|
605
|
+
const rows = await table
|
|
606
|
+
.query()
|
|
607
|
+
.where(`(${whereExpr}) AND metadataJson NOT LIKE '%"status":"merged"%'`)
|
|
608
|
+
.select([
|
|
609
|
+
"id",
|
|
610
|
+
"text",
|
|
611
|
+
"vector",
|
|
612
|
+
"category",
|
|
613
|
+
"scope",
|
|
614
|
+
"importance",
|
|
615
|
+
"timestamp",
|
|
616
|
+
"lastRecalled",
|
|
617
|
+
"recallCount",
|
|
618
|
+
"projectCount",
|
|
619
|
+
"schemaVersion",
|
|
620
|
+
"embeddingModel",
|
|
621
|
+
"vectorDim",
|
|
622
|
+
"metadataJson",
|
|
623
|
+
])
|
|
624
|
+
.limit(100000)
|
|
625
|
+
.toArray();
|
|
626
|
+
return rows
|
|
627
|
+
.map((row) => normalizeRow(row))
|
|
628
|
+
.filter((row) => row !== null);
|
|
629
|
+
}
|
|
502
630
|
async ensureIndexes() {
|
|
503
631
|
const table = this.requireTable();
|
|
504
632
|
try {
|
|
@@ -747,3 +875,11 @@ function extractRecalledProjects(metadataJson) {
|
|
|
747
875
|
}
|
|
748
876
|
return new Set();
|
|
749
877
|
}
|
|
878
|
+
function parseMetadata(metadataJson) {
|
|
879
|
+
try {
|
|
880
|
+
return JSON.parse(metadataJson);
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return {};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ContentType, ContentDetection, SummarizedContent, SummarizationConfig, InjectionConfig, SearchResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detects whether content contains code and its type
|
|
4
|
+
*/
|
|
5
|
+
export declare function detectContentType(text: string): ContentDetection;
|
|
6
|
+
/**
|
|
7
|
+
* Calculates bracket balance for code detection
|
|
8
|
+
*/
|
|
9
|
+
export declare function calculateBracketBalance(text: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* Counts code-related keywords
|
|
12
|
+
*/
|
|
13
|
+
export declare function countCodeKeywords(text: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Calculates ratio of indented lines
|
|
16
|
+
*/
|
|
17
|
+
export declare function calculateIndentationRatio(text: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Estimates token count for content
|
|
20
|
+
*/
|
|
21
|
+
export declare function estimateTokens(text: string, contentType: ContentType): number;
|
|
22
|
+
/**
|
|
23
|
+
* Truncates text to max characters
|
|
24
|
+
*/
|
|
25
|
+
export declare function truncateText(text: string, maxChars: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* Smart truncation for code - finds complete statement boundaries
|
|
28
|
+
*/
|
|
29
|
+
export declare function smartTruncateCode(code: string, maxLines: number, config?: {
|
|
30
|
+
preserveComments?: boolean;
|
|
31
|
+
preserveImports?: boolean;
|
|
32
|
+
}): string;
|
|
33
|
+
/**
|
|
34
|
+
* Extracts key sentences from text
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractKeySentences(text: string, targetChars: number): string;
|
|
37
|
+
export declare function splitCodeAndText(text: string): Array<{
|
|
38
|
+
type: "code" | "text";
|
|
39
|
+
content: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Main summarization function
|
|
43
|
+
*/
|
|
44
|
+
export declare function summarizeContent(text: string, config: SummarizationConfig): SummarizedContent;
|
|
45
|
+
/**
|
|
46
|
+
* Calculates injection limit based on mode
|
|
47
|
+
*/
|
|
48
|
+
export declare function calculateInjectionLimit(results: SearchResult[], config: InjectionConfig): number;
|
|
49
|
+
/**
|
|
50
|
+
* Creates default summarization config from injection config
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSummarizationConfig(injection: InjectionConfig): SummarizationConfig;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Code keywords used for content detection
|
|
2
|
+
const CODE_KEYWORDS = [
|
|
3
|
+
"function", "async", "await", "const", "let", "var", "return", "class", "interface", "type",
|
|
4
|
+
"import", "export", "from", "default", "extends", "implements", "new", "this", "super",
|
|
5
|
+
"def ", "async def", "func ", "fn ", "pub fn", "impl ", "struct ", "enum ",
|
|
6
|
+
"=>", "->", "::", "if (", "for (", "while (", "try {", "catch (", "throw ",
|
|
7
|
+
];
|
|
8
|
+
// Keywords for key sentence extraction
|
|
9
|
+
const KEY_SENTENCE_PATTERNS = [
|
|
10
|
+
/(?:fixed|resolved|works?\s+now|successful|done|完成|已解決|修復|成功)/i,
|
|
11
|
+
/(?:probleme|issue|bug|error|fail|錯誤|問題|失敗)/i,
|
|
12
|
+
/(?:solution|fix|resolve|解決方案|修正)/i,
|
|
13
|
+
/(?:because|root\s+cause|原因|由於)/i,
|
|
14
|
+
/(?:decide|decision|tradeoff|architecture|決定|架構|採用)/i,
|
|
15
|
+
/(?:prefer|preference|偏好|習慣)/i,
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Detects whether content contains code and its type
|
|
19
|
+
*/
|
|
20
|
+
export function detectContentType(text) {
|
|
21
|
+
const hasMarkdownCode = /```[\s\S]*?```/.test(text);
|
|
22
|
+
const bracketBalance = calculateBracketBalance(text);
|
|
23
|
+
const codeKeywords = countCodeKeywords(text);
|
|
24
|
+
const indentationRatio = calculateIndentationRatio(text);
|
|
25
|
+
const codeScore = (hasMarkdownCode ? 2 : 0) +
|
|
26
|
+
(bracketBalance > 3 ? 1 : 0) +
|
|
27
|
+
(codeKeywords > 5 ? 1 : 0) +
|
|
28
|
+
(indentationRatio > 0.3 ? 1 : 0);
|
|
29
|
+
if (codeScore >= 5) {
|
|
30
|
+
return { hasCode: true, isPureCode: true };
|
|
31
|
+
}
|
|
32
|
+
if (codeScore >= 3) {
|
|
33
|
+
return { hasCode: true, isPureCode: false };
|
|
34
|
+
}
|
|
35
|
+
if (hasMarkdownCode || codeKeywords > 10) {
|
|
36
|
+
return { hasCode: true, isPureCode: false };
|
|
37
|
+
}
|
|
38
|
+
return { hasCode: false, isPureCode: false };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculates bracket balance for code detection
|
|
42
|
+
*/
|
|
43
|
+
export function calculateBracketBalance(text) {
|
|
44
|
+
const openBrackets = (text.match(/[{([]/g) || []).length;
|
|
45
|
+
const closeBrackets = (text.match(/[})\]]/g) || []).length;
|
|
46
|
+
return Math.abs(openBrackets - closeBrackets) + Math.min(openBrackets, closeBrackets);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Counts code-related keywords
|
|
50
|
+
*/
|
|
51
|
+
export function countCodeKeywords(text) {
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
let count = 0;
|
|
54
|
+
for (const keyword of CODE_KEYWORDS) {
|
|
55
|
+
if (lower.includes(keyword.toLowerCase())) {
|
|
56
|
+
count += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Calculates ratio of indented lines
|
|
63
|
+
*/
|
|
64
|
+
export function calculateIndentationRatio(text) {
|
|
65
|
+
const lines = text.split("\n");
|
|
66
|
+
if (lines.length === 0)
|
|
67
|
+
return 0;
|
|
68
|
+
const indentedLines = lines.filter((line) => /^\s{2,}/.test(line));
|
|
69
|
+
return indentedLines.length / lines.length;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Estimates token count for content
|
|
73
|
+
*/
|
|
74
|
+
export function estimateTokens(text, contentType) {
|
|
75
|
+
// Count Chinese characters
|
|
76
|
+
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
|
|
77
|
+
const nonChineseChars = text.length - chineseChars;
|
|
78
|
+
// Chinese ~2 chars/token, English/other ~4 chars/token
|
|
79
|
+
const baseTokens = Math.ceil(chineseChars / 2 + nonChineseChars / 4);
|
|
80
|
+
// Code has higher token density
|
|
81
|
+
if (contentType === "code") {
|
|
82
|
+
return Math.ceil(baseTokens * 1.2);
|
|
83
|
+
}
|
|
84
|
+
return baseTokens;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Truncates text to max characters
|
|
88
|
+
*/
|
|
89
|
+
export function truncateText(text, maxChars) {
|
|
90
|
+
if (text.length <= maxChars)
|
|
91
|
+
return text;
|
|
92
|
+
return `${text.slice(0, maxChars - 3)}...`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Smart truncation for code - finds complete statement boundaries
|
|
96
|
+
*/
|
|
97
|
+
export function smartTruncateCode(code, maxLines, config) {
|
|
98
|
+
const lines = code.split("\n");
|
|
99
|
+
if (lines.length <= maxLines)
|
|
100
|
+
return code;
|
|
101
|
+
let braceBalance = 0;
|
|
102
|
+
let lastCompleteIndex = maxLines;
|
|
103
|
+
let foundComplete = false;
|
|
104
|
+
// Calculate brace balance and find last complete statement
|
|
105
|
+
for (let i = 0; i < Math.min(lines.length, maxLines + 10); i++) {
|
|
106
|
+
const line = lines[i];
|
|
107
|
+
braceBalance += (line.match(/{/g) || []).length;
|
|
108
|
+
braceBalance -= (line.match(/}/g) || []).length;
|
|
109
|
+
if (i >= maxLines - 5 && braceBalance === 0 && i < lines.length - 1) {
|
|
110
|
+
lastCompleteIndex = i + 1;
|
|
111
|
+
foundComplete = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// If no complete boundary found, use maxLines
|
|
116
|
+
if (!foundComplete) {
|
|
117
|
+
lastCompleteIndex = maxLines;
|
|
118
|
+
}
|
|
119
|
+
// Build truncated code
|
|
120
|
+
let result = lines.slice(0, lastCompleteIndex).join("\n");
|
|
121
|
+
// Add truncation indicator
|
|
122
|
+
result += "\n// ... (truncated)";
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extracts key sentences from text
|
|
127
|
+
*/
|
|
128
|
+
export function extractKeySentences(text, targetChars) {
|
|
129
|
+
const sentences = text.split(/[。.!?\n]+/).filter((s) => s.trim().length > 0);
|
|
130
|
+
const keySentences = [];
|
|
131
|
+
let currentLength = 0;
|
|
132
|
+
// First pass: sentences matching key patterns
|
|
133
|
+
for (const sentence of sentences) {
|
|
134
|
+
const trimmed = sentence.trim();
|
|
135
|
+
if (KEY_SENTENCE_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
136
|
+
if (currentLength + trimmed.length > targetChars && keySentences.length > 0) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
keySentences.push(trimmed);
|
|
140
|
+
currentLength += trimmed.length + 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Second pass: fill remaining with first sentences if needed
|
|
144
|
+
if (currentLength < targetChars * 0.5) {
|
|
145
|
+
for (const sentence of sentences) {
|
|
146
|
+
const trimmed = sentence.trim();
|
|
147
|
+
if (!keySentences.includes(trimmed)) {
|
|
148
|
+
if (currentLength + trimmed.length > targetChars) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
keySentences.push(trimmed);
|
|
152
|
+
currentLength += trimmed.length + 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return keySentences.join(" → ");
|
|
157
|
+
}
|
|
158
|
+
export function splitCodeAndText(text) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
161
|
+
let lastIndex = 0;
|
|
162
|
+
let match = codeBlockRegex.exec(text);
|
|
163
|
+
while (match !== null) {
|
|
164
|
+
if (match.index > lastIndex) {
|
|
165
|
+
const textPart = text.slice(lastIndex, match.index).trim();
|
|
166
|
+
if (textPart) {
|
|
167
|
+
parts.push({ type: "text", content: textPart });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
parts.push({ type: "code", content: match[0] });
|
|
171
|
+
lastIndex = match.index + match[0].length;
|
|
172
|
+
match = codeBlockRegex.exec(text);
|
|
173
|
+
}
|
|
174
|
+
if (lastIndex < text.length) {
|
|
175
|
+
const remaining = text.slice(lastIndex).trim();
|
|
176
|
+
if (remaining) {
|
|
177
|
+
parts.push({ type: "text", content: remaining });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return parts;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Main summarization function
|
|
184
|
+
*/
|
|
185
|
+
export function summarizeContent(text, config) {
|
|
186
|
+
const detection = detectContentType(text);
|
|
187
|
+
const originalLength = text.length;
|
|
188
|
+
// Determine content type
|
|
189
|
+
const contentType = detection.isPureCode
|
|
190
|
+
? "code"
|
|
191
|
+
: detection.hasCode
|
|
192
|
+
? "mixed"
|
|
193
|
+
: "text";
|
|
194
|
+
// No summarization
|
|
195
|
+
if (config.mode === "none") {
|
|
196
|
+
return {
|
|
197
|
+
type: "kept",
|
|
198
|
+
content: truncateText(text, config.textThreshold * 4), // Max chars limit
|
|
199
|
+
originalLength,
|
|
200
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Pure text
|
|
204
|
+
if (contentType === "text") {
|
|
205
|
+
if (text.length <= config.textThreshold) {
|
|
206
|
+
return {
|
|
207
|
+
type: "kept",
|
|
208
|
+
content: text,
|
|
209
|
+
originalLength,
|
|
210
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (config.mode === "truncate") {
|
|
214
|
+
const truncated = truncateText(text, config.summaryTargetChars);
|
|
215
|
+
return {
|
|
216
|
+
type: "truncated",
|
|
217
|
+
content: truncated,
|
|
218
|
+
originalLength,
|
|
219
|
+
estimatedTokens: estimateTokens(truncated, contentType),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const extracted = extractKeySentences(text, config.summaryTargetChars);
|
|
223
|
+
return {
|
|
224
|
+
type: "summarized",
|
|
225
|
+
content: extracted,
|
|
226
|
+
originalLength,
|
|
227
|
+
estimatedTokens: estimateTokens(extracted, contentType),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Pure code
|
|
231
|
+
if (contentType === "code") {
|
|
232
|
+
if (text.length <= config.codeThreshold) {
|
|
233
|
+
return {
|
|
234
|
+
type: "kept",
|
|
235
|
+
content: text,
|
|
236
|
+
originalLength,
|
|
237
|
+
estimatedTokens: estimateTokens(text, contentType),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const truncated = smartTruncateCode(text, config.maxCodeLines, {
|
|
241
|
+
preserveComments: config.preserveComments,
|
|
242
|
+
preserveImports: config.preserveImports,
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
type: "truncated",
|
|
246
|
+
content: truncated,
|
|
247
|
+
originalLength,
|
|
248
|
+
estimatedTokens: estimateTokens(truncated, contentType),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Mixed content
|
|
252
|
+
if (config.mode === "auto" || config.mode === "extract") {
|
|
253
|
+
const parts = splitCodeAndText(text);
|
|
254
|
+
const summarizedParts = [];
|
|
255
|
+
for (const part of parts) {
|
|
256
|
+
if (part.type === "text") {
|
|
257
|
+
if (part.content.length <= config.textThreshold) {
|
|
258
|
+
summarizedParts.push(part.content);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
summarizedParts.push(extractKeySentences(part.content, config.summaryTargetChars / 2));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (part.content.length <= config.codeThreshold) {
|
|
266
|
+
summarizedParts.push(part.content);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
summarizedParts.push(smartTruncateCode(part.content, config.maxCodeLines));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
type: "mixed",
|
|
275
|
+
content: summarizedParts.join("\n\n"),
|
|
276
|
+
originalLength,
|
|
277
|
+
estimatedTokens: estimateTokens(summarizedParts.join("\n\n"), contentType),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Fallback: truncate
|
|
281
|
+
return {
|
|
282
|
+
type: "truncated",
|
|
283
|
+
content: truncateText(text, config.summaryTargetChars),
|
|
284
|
+
originalLength,
|
|
285
|
+
estimatedTokens: estimateTokens(truncateText(text, config.summaryTargetChars), contentType),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Calculates injection limit based on mode
|
|
290
|
+
*/
|
|
291
|
+
export function calculateInjectionLimit(results, config) {
|
|
292
|
+
// Filter by injection floor
|
|
293
|
+
const filteredResults = results.filter((r) => r.score >= config.injectionFloor);
|
|
294
|
+
// Fixed mode: simple limit
|
|
295
|
+
if (config.mode === "fixed") {
|
|
296
|
+
return Math.min(config.maxMemories, filteredResults.length);
|
|
297
|
+
}
|
|
298
|
+
// Budget mode: accumulate until budget exhausted
|
|
299
|
+
if (config.mode === "budget") {
|
|
300
|
+
let accumulatedTokens = 0;
|
|
301
|
+
let count = 0;
|
|
302
|
+
for (const result of filteredResults) {
|
|
303
|
+
const tokens = estimateTokens(result.record.text, detectContentType(result.record.text).isPureCode ? "code" : "text");
|
|
304
|
+
if (accumulatedTokens + tokens > config.budgetTokens && count >= config.minMemories) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
accumulatedTokens += tokens;
|
|
308
|
+
count += 1;
|
|
309
|
+
if (count >= config.maxMemories) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return Math.max(config.minMemories, Math.min(count, config.maxMemories));
|
|
314
|
+
}
|
|
315
|
+
// Adaptive mode: stop on score drop
|
|
316
|
+
if (config.mode === "adaptive") {
|
|
317
|
+
let count = 0;
|
|
318
|
+
let prevScore = filteredResults[0]?.score ?? 0;
|
|
319
|
+
for (const result of filteredResults) {
|
|
320
|
+
const scoreDrop = prevScore - result.score;
|
|
321
|
+
// Stop if score drops below tolerance (but respect minimum)
|
|
322
|
+
if (scoreDrop > config.scoreDropTolerance && count >= config.minMemories) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
count += 1;
|
|
326
|
+
prevScore = result.score;
|
|
327
|
+
if (count >= config.maxMemories) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return Math.max(config.minMemories, Math.min(count, filteredResults.length));
|
|
332
|
+
}
|
|
333
|
+
// Fallback
|
|
334
|
+
return Math.min(config.maxMemories, filteredResults.length);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Creates default summarization config from injection config
|
|
338
|
+
*/
|
|
339
|
+
export function createSummarizationConfig(injection) {
|
|
340
|
+
return {
|
|
341
|
+
mode: injection.summarization,
|
|
342
|
+
textThreshold: 300,
|
|
343
|
+
codeThreshold: injection.codeSummarization.pureCodeThreshold,
|
|
344
|
+
summaryTargetChars: injection.summaryTargetChars,
|
|
345
|
+
maxCodeLines: injection.codeSummarization.maxCodeLines,
|
|
346
|
+
codeTruncationMode: injection.codeSummarization.codeTruncationMode,
|
|
347
|
+
preserveComments: injection.codeSummarization.preserveComments,
|
|
348
|
+
preserveImports: injection.codeSummarization.preserveImports,
|
|
349
|
+
};
|
|
350
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
export type EmbeddingProvider = "ollama" | "openai";
|
|
2
2
|
export type RetrievalMode = "hybrid" | "vector";
|
|
3
|
+
export type InjectionMode = "fixed" | "budget" | "adaptive";
|
|
4
|
+
export type SummarizationMode = "none" | "truncate" | "extract" | "auto";
|
|
5
|
+
export type CodeTruncationMode = "smart" | "signature" | "preserve";
|
|
6
|
+
export type ContentType = "text" | "code" | "mixed";
|
|
7
|
+
export interface ContentDetection {
|
|
8
|
+
hasCode: boolean;
|
|
9
|
+
isPureCode: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SummarizedContent {
|
|
12
|
+
type: "kept" | "truncated" | "summarized" | "mixed";
|
|
13
|
+
content: string;
|
|
14
|
+
originalLength: number;
|
|
15
|
+
estimatedTokens: number;
|
|
16
|
+
}
|
|
3
17
|
export type MemoryCategory = "preference" | "fact" | "decision" | "entity" | "other";
|
|
4
18
|
export type CaptureOutcome = "considered" | "skipped" | "stored";
|
|
5
|
-
export type CaptureSkipReason = "empty-buffer" | "below-min-chars" | "no-positive-signal" | "initialization-unavailable" | "embedding-unavailable" | "empty-embedding";
|
|
19
|
+
export type CaptureSkipReason = "empty-buffer" | "below-min-chars" | "no-positive-signal" | "initialization-unavailable" | "embedding-unavailable" | "empty-embedding" | "duplicate-similarity" | "duplicate-exact";
|
|
6
20
|
export type FeedbackType = "missing" | "wrong" | "useful";
|
|
7
21
|
export type RecallSource = "system-transform" | "manual-search";
|
|
8
22
|
export type MemoryScope = "project" | "global";
|
|
@@ -23,11 +37,48 @@ export interface RetrievalConfig {
|
|
|
23
37
|
recencyHalfLifeHours: number;
|
|
24
38
|
importanceWeight: number;
|
|
25
39
|
}
|
|
40
|
+
export interface CodeSummarizationConfig {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
pureCodeThreshold: number;
|
|
43
|
+
maxCodeLines: number;
|
|
44
|
+
codeTruncationMode: CodeTruncationMode;
|
|
45
|
+
preserveComments: boolean;
|
|
46
|
+
preserveImports: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface InjectionConfig {
|
|
49
|
+
mode: InjectionMode;
|
|
50
|
+
maxMemories: number;
|
|
51
|
+
minMemories: number;
|
|
52
|
+
budgetTokens: number;
|
|
53
|
+
maxCharsPerMemory: number;
|
|
54
|
+
summarization: SummarizationMode;
|
|
55
|
+
summaryTargetChars: number;
|
|
56
|
+
scoreDropTolerance: number;
|
|
57
|
+
injectionFloor: number;
|
|
58
|
+
codeSummarization: CodeSummarizationConfig;
|
|
59
|
+
}
|
|
60
|
+
export interface SummarizationConfig {
|
|
61
|
+
mode: SummarizationMode;
|
|
62
|
+
textThreshold: number;
|
|
63
|
+
codeThreshold: number;
|
|
64
|
+
summaryTargetChars: number;
|
|
65
|
+
maxCodeLines: number;
|
|
66
|
+
codeTruncationMode: CodeTruncationMode;
|
|
67
|
+
preserveComments: boolean;
|
|
68
|
+
preserveImports: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface DedupConfig {
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
writeThreshold: number;
|
|
73
|
+
consolidateThreshold: number;
|
|
74
|
+
}
|
|
26
75
|
export interface MemoryRuntimeConfig {
|
|
27
76
|
provider: string;
|
|
28
77
|
dbPath: string;
|
|
29
78
|
embedding: EmbeddingConfig;
|
|
30
79
|
retrieval: RetrievalConfig;
|
|
80
|
+
injection: InjectionConfig;
|
|
81
|
+
dedup: DedupConfig;
|
|
31
82
|
includeGlobalScope: boolean;
|
|
32
83
|
globalDetectionThreshold: number;
|
|
33
84
|
globalDiscountFactor: number;
|
|
@@ -135,5 +186,9 @@ export interface EffectivenessSummary {
|
|
|
135
186
|
falsePositiveRate: number;
|
|
136
187
|
falseNegativeRate: number;
|
|
137
188
|
};
|
|
189
|
+
duplicates: {
|
|
190
|
+
flaggedCount: number;
|
|
191
|
+
consolidatedCount: number;
|
|
192
|
+
};
|
|
138
193
|
}
|
|
139
194
|
export {};
|