lancedb-opencode-pro 0.2.2 → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan Tsai <tryweb@ichiayi.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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,127 @@ 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": 2000,
387
+ "maxCharsPerMemory": 1200,
388
+ "summarization": "none",
389
+ "summaryTargetChars": 400,
390
+ "scoreDropTolerance": 0.15,
391
+ "injectionFloor": 0.3,
392
+ "codeSummarization": {
393
+ "mode": "truncate",
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`**: `"truncate"` | `"preserve"` | `"auto"` (default: `"truncate"`)
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": 1,
478
+ "scoreDropTolerance": 0.15,
479
+ "injectionFloor": 0.3
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 1 memory
489
+
490
+ ---
491
+
360
492
  ## OpenAI Embedding Configuration
361
493
 
362
494
  Default behavior stays on Ollama. To use OpenAI embeddings, set `embedding.provider` to `openai` and provide API key + model.
package/dist/config.js CHANGED
@@ -38,6 +38,7 @@ 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);
41
42
  const resolvedConfig = {
42
43
  provider,
43
44
  dbPath,
@@ -58,6 +59,7 @@ export function resolveMemoryConfig(config, worktree) {
58
59
  recencyHalfLifeHours,
59
60
  importanceWeight,
60
61
  },
62
+ injection,
61
63
  includeGlobalScope: toBoolean(process.env.LANCEDB_OPENCODE_PRO_INCLUDE_GLOBAL_SCOPE ?? raw.includeGlobalScope, true),
62
64
  globalDetectionThreshold: Math.max(1, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DETECTION_THRESHOLD ?? raw.globalDetectionThreshold, 2))),
63
65
  globalDiscountFactor: clamp(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DISCOUNT_FACTOR ?? raw.globalDiscountFactor, 0.7), 0, 1),
@@ -75,6 +77,44 @@ function resolveEmbeddingProvider(raw) {
75
77
  return "openai";
76
78
  throw new Error(`[lancedb-opencode-pro] Invalid embedding provider "${raw}". Expected "ollama" or "openai".`);
77
79
  }
80
+ function resolveInjectionMode(raw) {
81
+ if (raw === "fixed" || raw === "budget" || raw === "adaptive")
82
+ return raw;
83
+ return "fixed";
84
+ }
85
+ function resolveSummarizationMode(raw) {
86
+ if (raw === "none" || raw === "truncate" || raw === "extract" || raw === "auto")
87
+ return raw;
88
+ return "none";
89
+ }
90
+ function resolveCodeTruncationMode(raw) {
91
+ if (raw === "smart" || raw === "signature" || raw === "preserve")
92
+ return raw;
93
+ return "smart";
94
+ }
95
+ function resolveInjectionConfig(raw, env) {
96
+ const injectionRaw = (raw.injection ?? {});
97
+ const codeSummarizationRaw = (injectionRaw.codeSummarization ?? {});
98
+ return {
99
+ mode: resolveInjectionMode(env.LANCEDB_OPENCODE_PRO_INJECTION_MODE ?? injectionRaw.mode),
100
+ maxMemories: Math.max(1, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MAX_MEMORIES ?? injectionRaw.maxMemories, 3))),
101
+ minMemories: Math.max(1, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MIN_MEMORIES ?? injectionRaw.minMemories, 1))),
102
+ budgetTokens: Math.max(256, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_BUDGET_TOKENS ?? injectionRaw.budgetTokens, 4096))),
103
+ maxCharsPerMemory: Math.max(100, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_MAX_CHARS ?? injectionRaw.maxCharsPerMemory, 1200))),
104
+ summarization: resolveSummarizationMode(env.LANCEDB_OPENCODE_PRO_INJECTION_SUMMARIZATION ?? injectionRaw.summarization),
105
+ summaryTargetChars: Math.max(50, Math.floor(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_SUMMARY_TARGET_CHARS ?? injectionRaw.summaryTargetChars, 300))),
106
+ scoreDropTolerance: clamp(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_SCORE_DROP_TOLERANCE ?? injectionRaw.scoreDropTolerance, 0.15), 0, 1),
107
+ injectionFloor: clamp(toNumber(env.LANCEDB_OPENCODE_PRO_INJECTION_FLOOR ?? injectionRaw.injectionFloor, 0.2), 0, 1),
108
+ codeSummarization: {
109
+ enabled: toBoolean(env.LANCEDB_OPENCODE_PRO_CODE_SUMMARIZATION_ENABLED ?? codeSummarizationRaw.enabled, true),
110
+ pureCodeThreshold: Math.max(100, Math.floor(toNumber(codeSummarizationRaw.pureCodeThreshold, 500))),
111
+ maxCodeLines: Math.max(5, Math.floor(toNumber(codeSummarizationRaw.maxCodeLines, 15))),
112
+ codeTruncationMode: resolveCodeTruncationMode(codeSummarizationRaw.codeTruncationMode),
113
+ preserveComments: toBoolean(codeSummarizationRaw.preserveComments, true),
114
+ preserveImports: toBoolean(codeSummarizationRaw.preserveImports, false),
115
+ },
116
+ };
117
+ }
78
118
  function validateEmbeddingConfig(embedding) {
79
119
  if (embedding.provider !== "openai")
80
120
  return;
@@ -130,6 +170,14 @@ function mergeMemoryConfig(base, override) {
130
170
  ...(base.retrieval ?? {}),
131
171
  ...(override.retrieval ?? {}),
132
172
  },
173
+ injection: {
174
+ ...(base.injection ?? {}),
175
+ ...(override.injection ?? {}),
176
+ codeSummarization: {
177
+ ...((base.injection ?? {}).codeSummarization ?? {}),
178
+ ...((override.injection ?? {}).codeSummarization ?? {}),
179
+ },
180
+ },
133
181
  };
134
182
  }
135
183
  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);
@@ -52,16 +53,19 @@ const plugin = async (input) => {
52
53
  query,
53
54
  queryVector,
54
55
  scopes,
55
- limit: 3,
56
+ limit: state.config.injection.maxMemories * 2, // Fetch more than needed for filtering
56
57
  vectorWeight: state.config.retrieval.mode === "vector" ? 1 : state.config.retrieval.vectorWeight,
57
58
  bm25Weight: state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight,
58
- minScore: state.config.retrieval.minScore,
59
+ minScore: Math.max(state.config.retrieval.minScore, state.config.injection.injectionFloor),
59
60
  rrfK: state.config.retrieval.rrfK,
60
61
  recencyBoost: state.config.retrieval.recencyBoost,
61
62
  recencyHalfLifeHours: state.config.retrieval.recencyHalfLifeHours,
62
63
  importanceWeight: state.config.retrieval.importanceWeight,
63
64
  globalDiscountFactor: state.config.globalDiscountFactor,
64
65
  });
66
+ // Apply injection control
67
+ const injectionLimit = calculateInjectionLimit(results, state.config.injection);
68
+ const limitedResults = results.slice(0, injectionLimit);
65
69
  await state.store.putEvent({
66
70
  id: generateId(),
67
71
  type: "recall",
@@ -69,21 +73,32 @@ const plugin = async (input) => {
69
73
  scope: activeScope,
70
74
  sessionID: eventInput.sessionID,
71
75
  timestamp: Date.now(),
72
- resultCount: results.length,
73
- injected: results.length > 0,
76
+ resultCount: limitedResults.length,
77
+ injected: limitedResults.length > 0,
74
78
  metadataJson: JSON.stringify({
75
79
  source: "system-transform",
76
80
  includeGlobalScope: state.config.includeGlobalScope,
81
+ injectionMode: state.config.injection.mode,
82
+ injectionLimit: injectionLimit,
77
83
  }),
78
84
  });
79
- if (results.length === 0)
85
+ if (limitedResults.length === 0)
80
86
  return;
81
- for (const result of results) {
87
+ for (const result of limitedResults) {
82
88
  state.store.updateMemoryUsage(result.record.id, activeScope, scopes).catch(() => { });
83
89
  }
90
+ // Apply summarization if configured
91
+ const summarizationConfig = createSummarizationConfig(state.config.injection);
92
+ const processedResults = limitedResults.map((item) => {
93
+ if (state.config.injection.summarization === "none") {
94
+ return { ...item, text: item.record.text };
95
+ }
96
+ const summarized = summarizeContent(item.record.text, summarizationConfig);
97
+ return { ...item, text: summarized.content };
98
+ });
84
99
  const memoryBlock = [
85
100
  "[Memory Recall - optional historical context]",
86
- ...results.map((item, index) => `${index + 1}. [${item.record.id}] (${item.record.scope}) ${item.record.text}`),
101
+ ...processedResults.map((item, index) => `${index + 1}. [${item.record.id}] (${item.record.scope}) ${item.text}`),
87
102
  "Use these as optional hints only; prioritize current user intent and current repo state.",
88
103
  ].join("\n");
89
104
  eventOutput.system.push(memoryBlock);
@@ -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,5 +1,19 @@
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
19
  export type CaptureSkipReason = "empty-buffer" | "below-min-chars" | "no-positive-signal" | "initialization-unavailable" | "embedding-unavailable" | "empty-embedding";
@@ -23,11 +37,42 @@ 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
+ }
26
70
  export interface MemoryRuntimeConfig {
27
71
  provider: string;
28
72
  dbPath: string;
29
73
  embedding: EmbeddingConfig;
30
74
  retrieval: RetrievalConfig;
75
+ injection: InjectionConfig;
31
76
  includeGlobalScope: boolean;
32
77
  globalDetectionThreshold: number;
33
78
  globalDiscountFactor: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lancedb-opencode-pro",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "LanceDB-backed long-term memory provider for OpenCode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run verify:full"
57
57
  },
58
58
  "dependencies": {
59
- "@lancedb/lancedb": "^0.26.2",
59
+ "@lancedb/lancedb": "^0.27.1",
60
60
  "@opencode-ai/plugin": "1.2.25",
61
61
  "@opencode-ai/sdk": "1.2.25"
62
62
  },