chainlesschain 0.37.12 → 0.40.1
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/package.json +3 -2
- package/src/commands/agent.js +7 -1
- package/src/commands/ask.js +24 -9
- package/src/commands/chat.js +7 -1
- package/src/commands/cli-anything.js +266 -0
- package/src/commands/compliance.js +216 -0
- package/src/commands/dao.js +312 -0
- package/src/commands/dlp.js +278 -0
- package/src/commands/evomap.js +558 -0
- package/src/commands/hardening.js +230 -0
- package/src/commands/matrix.js +168 -0
- package/src/commands/nostr.js +185 -0
- package/src/commands/pqc.js +162 -0
- package/src/commands/scim.js +218 -0
- package/src/commands/serve.js +109 -0
- package/src/commands/siem.js +156 -0
- package/src/commands/social.js +480 -0
- package/src/commands/terraform.js +148 -0
- package/src/constants.js +1 -0
- package/src/index.js +60 -0
- package/src/lib/autonomous-agent.js +487 -0
- package/src/lib/cli-anything-bridge.js +379 -0
- package/src/lib/cli-context-engineering.js +472 -0
- package/src/lib/compliance-manager.js +290 -0
- package/src/lib/content-recommender.js +205 -0
- package/src/lib/dao-governance.js +296 -0
- package/src/lib/dlp-engine.js +304 -0
- package/src/lib/evomap-client.js +135 -0
- package/src/lib/evomap-federation.js +240 -0
- package/src/lib/evomap-governance.js +250 -0
- package/src/lib/evomap-manager.js +227 -0
- package/src/lib/git-integration.js +1 -1
- package/src/lib/hardening-manager.js +275 -0
- package/src/lib/llm-providers.js +14 -1
- package/src/lib/matrix-bridge.js +196 -0
- package/src/lib/nostr-bridge.js +195 -0
- package/src/lib/permanent-memory.js +370 -0
- package/src/lib/plan-mode.js +211 -0
- package/src/lib/pqc-manager.js +196 -0
- package/src/lib/scim-manager.js +212 -0
- package/src/lib/session-manager.js +38 -0
- package/src/lib/siem-exporter.js +137 -0
- package/src/lib/social-manager.js +283 -0
- package/src/lib/task-model-selector.js +232 -0
- package/src/lib/terraform-manager.js +201 -0
- package/src/lib/ws-server.js +474 -0
- package/src/repl/agent-repl.js +796 -41
- package/src/repl/chat-repl.js +14 -6
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Context Engineering — lightweight adapter for agent-repl.
|
|
3
|
+
*
|
|
4
|
+
* Integrates 5 context injectors (Instinct / Memory / BM25 Notes / Task Reminder / Permanent Memory)
|
|
5
|
+
* with KV-Cache-friendly system prompt cleaning, importance-based compaction,
|
|
6
|
+
* resumable compaction summaries, and stable prefix caching.
|
|
7
|
+
*
|
|
8
|
+
* Graceful degradation: works without DB (static prompt fallback).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { generateInstinctPrompt } from "./instinct-manager.js";
|
|
12
|
+
import { recallMemory } from "./hierarchical-memory.js";
|
|
13
|
+
import { BM25Search } from "./bm25-search.js";
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
|
|
16
|
+
// Exported for test injection
|
|
17
|
+
export const _deps = {
|
|
18
|
+
generateInstinctPrompt,
|
|
19
|
+
recallMemory,
|
|
20
|
+
BM25Search,
|
|
21
|
+
createHash,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ─── System prompt cleaning regexes (match desktop KV-Cache optimization) ───
|
|
25
|
+
const CLEAN_PATTERNS = [
|
|
26
|
+
{
|
|
27
|
+
pattern: /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[.\dZ+-]*/g,
|
|
28
|
+
replacement: "[DATE]",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
32
|
+
replacement: "[UUID]",
|
|
33
|
+
},
|
|
34
|
+
{ pattern: /session[-_]?[0-9a-f]{6,}/gi, replacement: "[SESSION]" },
|
|
35
|
+
{ pattern: /\b\d{10,13}\b/g, replacement: "[TIMESTAMP]" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export class CLIContextEngineering {
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} options
|
|
41
|
+
* @param {object|null} options.db - Database instance (null for graceful degradation)
|
|
42
|
+
* @param {object|null} options.permanentMemory - CLIPermanentMemory instance (optional)
|
|
43
|
+
*/
|
|
44
|
+
constructor({ db, permanentMemory } = {}) {
|
|
45
|
+
this.db = db || null;
|
|
46
|
+
this.permanentMemory = permanentMemory || null;
|
|
47
|
+
this.errorHistory = [];
|
|
48
|
+
this.taskContext = null;
|
|
49
|
+
this._bm25 = null;
|
|
50
|
+
this._notesIndexed = false;
|
|
51
|
+
// Resumable compaction: summaries of discarded message pairs
|
|
52
|
+
this._compactionSummaries = [];
|
|
53
|
+
// Stable prefix cache: { hash, cleanedPrefix }
|
|
54
|
+
this._prefixCache = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build optimized messages for LLM consumption.
|
|
59
|
+
* Returns a new array (does not modify input).
|
|
60
|
+
*
|
|
61
|
+
* @param {Array} rawMessages - Original messages array
|
|
62
|
+
* @param {object} options
|
|
63
|
+
* @param {string} [options.userQuery] - Latest user query for relevance matching
|
|
64
|
+
* @returns {Array} Optimized messages
|
|
65
|
+
*/
|
|
66
|
+
buildOptimizedMessages(rawMessages, { userQuery } = {}) {
|
|
67
|
+
const result = [];
|
|
68
|
+
|
|
69
|
+
// 1. System prompt — clean dynamic content for KV-Cache stability
|
|
70
|
+
let historyStart = 0;
|
|
71
|
+
if (rawMessages.length > 0 && rawMessages[0].role === "system") {
|
|
72
|
+
result.push({
|
|
73
|
+
role: "system",
|
|
74
|
+
content: this._cleanSystemPrompt(rawMessages[0].content),
|
|
75
|
+
});
|
|
76
|
+
historyStart = 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Instinct injection
|
|
80
|
+
if (this.db) {
|
|
81
|
+
try {
|
|
82
|
+
const instinctPrompt = _deps.generateInstinctPrompt(this.db);
|
|
83
|
+
if (instinctPrompt) {
|
|
84
|
+
result.push({
|
|
85
|
+
role: "system",
|
|
86
|
+
content: `## Learned Preferences\n${instinctPrompt}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (_err) {
|
|
90
|
+
// Instinct injection failed — skip silently
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Memory injection
|
|
95
|
+
if (this.db && userQuery) {
|
|
96
|
+
try {
|
|
97
|
+
const memories = _deps.recallMemory(this.db, userQuery, { limit: 5 });
|
|
98
|
+
if (memories && memories.length > 0) {
|
|
99
|
+
const lines = memories.map(
|
|
100
|
+
(m) =>
|
|
101
|
+
`- [${m.layer}] ${m.content} (retention: ${(m.retention * 100).toFixed(0)}%)`,
|
|
102
|
+
);
|
|
103
|
+
result.push({
|
|
104
|
+
role: "system",
|
|
105
|
+
content: `## Relevant Memories\n${lines.join("\n")}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} catch (_err) {
|
|
109
|
+
// Memory injection failed — skip silently
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. Notes injection (BM25 search)
|
|
114
|
+
if (this.db && userQuery) {
|
|
115
|
+
try {
|
|
116
|
+
this._ensureNotesIndex();
|
|
117
|
+
if (this._bm25 && this._bm25.totalDocs > 0) {
|
|
118
|
+
const hits = this._bm25.search(userQuery, {
|
|
119
|
+
topK: 3,
|
|
120
|
+
threshold: 0.5,
|
|
121
|
+
});
|
|
122
|
+
if (hits.length > 0) {
|
|
123
|
+
const lines = hits.map(
|
|
124
|
+
(h) =>
|
|
125
|
+
`- **${h.doc.title || "Untitled"}**: ${(h.doc.content || "").substring(0, 200)}`,
|
|
126
|
+
);
|
|
127
|
+
result.push({
|
|
128
|
+
role: "system",
|
|
129
|
+
content: `## Relevant Notes\n${lines.join("\n")}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (_err) {
|
|
134
|
+
// Notes injection failed — skip silently
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 5. Permanent memory injection
|
|
139
|
+
if (this.permanentMemory && userQuery) {
|
|
140
|
+
try {
|
|
141
|
+
const pmResults = this.permanentMemory.getRelevantContext(userQuery, 3);
|
|
142
|
+
if (pmResults && pmResults.length > 0) {
|
|
143
|
+
const lines = pmResults.map(
|
|
144
|
+
(r) => `- [${r.source || "memory"}] ${r.content}`,
|
|
145
|
+
);
|
|
146
|
+
result.push({
|
|
147
|
+
role: "system",
|
|
148
|
+
content: `## Permanent Memory\n${lines.join("\n")}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch (_err) {
|
|
152
|
+
// Permanent memory injection failed — skip silently
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 5b. Compaction summaries (resumable context from discarded messages)
|
|
157
|
+
if (this._compactionSummaries.length > 0) {
|
|
158
|
+
result.push({
|
|
159
|
+
role: "system",
|
|
160
|
+
content: `## Compacted Context Summary\n${this._compactionSummaries.join("\n")}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 6. Conversation history — clean metadata
|
|
165
|
+
for (let i = historyStart; i < rawMessages.length; i++) {
|
|
166
|
+
const msg = rawMessages[i];
|
|
167
|
+
const cleaned = { role: msg.role };
|
|
168
|
+
if (msg.content !== undefined) cleaned.content = msg.content;
|
|
169
|
+
if (msg.tool_calls) cleaned.tool_calls = msg.tool_calls;
|
|
170
|
+
if (msg.name) cleaned.name = msg.name;
|
|
171
|
+
if (msg.tool_call_id) cleaned.tool_call_id = msg.tool_call_id;
|
|
172
|
+
result.push(cleaned);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 7. Error context
|
|
176
|
+
if (this.errorHistory.length > 0) {
|
|
177
|
+
const recent = this.errorHistory.slice(-5);
|
|
178
|
+
const lines = recent.map(
|
|
179
|
+
(e) =>
|
|
180
|
+
`- [${e.step}] ${e.message}${e.resolution ? ` → Fixed: ${e.resolution}` : ""}`,
|
|
181
|
+
);
|
|
182
|
+
result.push({
|
|
183
|
+
role: "system",
|
|
184
|
+
content: `## Recent Errors\n${lines.join("\n")}\nAvoid repeating these mistakes.`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 8. Task reminder
|
|
189
|
+
if (this.taskContext) {
|
|
190
|
+
const tc = this.taskContext;
|
|
191
|
+
const stepLines = tc.steps
|
|
192
|
+
? tc.steps.map((s, i) => {
|
|
193
|
+
const status =
|
|
194
|
+
i < tc.currentStep
|
|
195
|
+
? "done"
|
|
196
|
+
: i === tc.currentStep
|
|
197
|
+
? "current"
|
|
198
|
+
: "pending";
|
|
199
|
+
return ` ${status === "done" ? "✓" : status === "current" ? "→" : "○"} ${s}`;
|
|
200
|
+
})
|
|
201
|
+
: [];
|
|
202
|
+
result.push({
|
|
203
|
+
role: "system",
|
|
204
|
+
content: [
|
|
205
|
+
"## Current Task Status",
|
|
206
|
+
`**Objective**: ${tc.objective}`,
|
|
207
|
+
...(stepLines.length > 0 ? ["**Progress**:", ...stepLines] : []),
|
|
208
|
+
"Stay focused on this objective.",
|
|
209
|
+
].join("\n"),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Record an error for context injection.
|
|
218
|
+
*/
|
|
219
|
+
recordError({ step, message, resolution }) {
|
|
220
|
+
this.errorHistory.push({ step, message, resolution, time: Date.now() });
|
|
221
|
+
if (this.errorHistory.length > 10) {
|
|
222
|
+
this.errorHistory.shift();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Set current task objective and steps.
|
|
228
|
+
*/
|
|
229
|
+
setTask(objective, steps = []) {
|
|
230
|
+
this.taskContext = {
|
|
231
|
+
objective,
|
|
232
|
+
steps,
|
|
233
|
+
currentStep: 0,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Update task progress.
|
|
239
|
+
*/
|
|
240
|
+
updateTaskProgress(step, _status) {
|
|
241
|
+
if (!this.taskContext) return;
|
|
242
|
+
if (typeof step === "number") {
|
|
243
|
+
this.taskContext.currentStep = step;
|
|
244
|
+
} else {
|
|
245
|
+
// Find step by name
|
|
246
|
+
const idx = this.taskContext.steps.indexOf(step);
|
|
247
|
+
if (idx >= 0) this.taskContext.currentStep = idx;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clear current task.
|
|
253
|
+
*/
|
|
254
|
+
clearTask() {
|
|
255
|
+
this.taskContext = null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Importance-based smart compaction.
|
|
260
|
+
* Keeps system prompt + top-scoring message pairs.
|
|
261
|
+
*
|
|
262
|
+
* @param {Array} messages - Full messages array
|
|
263
|
+
* @param {object} options
|
|
264
|
+
* @param {number} [options.keepPairs=6] - Number of user+assistant pairs to keep
|
|
265
|
+
* @returns {Array} Compacted messages
|
|
266
|
+
*/
|
|
267
|
+
smartCompact(messages, { keepPairs = 6 } = {}) {
|
|
268
|
+
if (messages.length <= 3) return [...messages];
|
|
269
|
+
|
|
270
|
+
// Always keep messages[0] (system prompt)
|
|
271
|
+
const systemMsg = messages[0];
|
|
272
|
+
const rest = messages.slice(1);
|
|
273
|
+
|
|
274
|
+
// Group into user+assistant pairs (+ tool messages attached to assistant)
|
|
275
|
+
const pairs = [];
|
|
276
|
+
let currentPair = [];
|
|
277
|
+
|
|
278
|
+
for (const msg of rest) {
|
|
279
|
+
if (msg.role === "user" && currentPair.length > 0) {
|
|
280
|
+
pairs.push(currentPair);
|
|
281
|
+
currentPair = [];
|
|
282
|
+
}
|
|
283
|
+
currentPair.push(msg);
|
|
284
|
+
}
|
|
285
|
+
if (currentPair.length > 0) {
|
|
286
|
+
pairs.push(currentPair);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (pairs.length <= keepPairs) return [...messages];
|
|
290
|
+
|
|
291
|
+
// Score each pair
|
|
292
|
+
const scored = pairs.map((pair, idx) => {
|
|
293
|
+
let score = 0;
|
|
294
|
+
|
|
295
|
+
// Recency bonus (higher index = more recent)
|
|
296
|
+
score += (idx / pairs.length) * 5;
|
|
297
|
+
|
|
298
|
+
// Tool calls bonus
|
|
299
|
+
if (pair.some((m) => m.tool_calls || m.role === "tool")) {
|
|
300
|
+
score += 2;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Task relevance bonus
|
|
304
|
+
if (this.taskContext) {
|
|
305
|
+
const objective = this.taskContext.objective.toLowerCase();
|
|
306
|
+
if (
|
|
307
|
+
pair.some(
|
|
308
|
+
(m) => m.content && m.content.toLowerCase().includes(objective),
|
|
309
|
+
)
|
|
310
|
+
) {
|
|
311
|
+
score += 3;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Error context bonus
|
|
316
|
+
if (pair.some((m) => m.content && m.content.includes("Error"))) {
|
|
317
|
+
score += 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { pair, score };
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Sort by score descending, keep top pairs
|
|
324
|
+
scored.sort((a, b) => b.score - a.score);
|
|
325
|
+
const kept = scored.slice(0, keepPairs);
|
|
326
|
+
const discarded = scored.slice(keepPairs);
|
|
327
|
+
|
|
328
|
+
// Generate one-line summaries for discarded pairs (resumable compaction)
|
|
329
|
+
for (const { pair } of discarded) {
|
|
330
|
+
const userMsg = pair.find((m) => m.role === "user");
|
|
331
|
+
const assistantMsg = pair.find((m) => m.role === "assistant");
|
|
332
|
+
if (userMsg && userMsg.content) {
|
|
333
|
+
const topic = userMsg.content.substring(0, 80).replace(/\n/g, " ");
|
|
334
|
+
const hadTools = pair.some((m) => m.tool_calls || m.role === "tool");
|
|
335
|
+
const summary = hadTools
|
|
336
|
+
? `- Q: "${topic}" → [used tools] ${(assistantMsg?.content || "").substring(0, 60).replace(/\n/g, " ")}`
|
|
337
|
+
: `- Q: "${topic}" → ${(assistantMsg?.content || "").substring(0, 80).replace(/\n/g, " ")}`;
|
|
338
|
+
this._compactionSummaries.push(summary);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Cap summaries at 20
|
|
342
|
+
if (this._compactionSummaries.length > 20) {
|
|
343
|
+
this._compactionSummaries = this._compactionSummaries.slice(-20);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Restore chronological order
|
|
347
|
+
kept.sort((a, b) => pairs.indexOf(a.pair) - pairs.indexOf(b.pair));
|
|
348
|
+
|
|
349
|
+
const result = [systemMsg];
|
|
350
|
+
for (const { pair } of kept) {
|
|
351
|
+
result.push(...pair);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get engine statistics.
|
|
359
|
+
*/
|
|
360
|
+
getStats() {
|
|
361
|
+
return {
|
|
362
|
+
hasDb: !!this.db,
|
|
363
|
+
hasPermanentMemory: !!this.permanentMemory,
|
|
364
|
+
errorCount: this.errorHistory.length,
|
|
365
|
+
hasTask: !!this.taskContext,
|
|
366
|
+
notesIndexed: this._bm25 ? this._bm25.totalDocs : 0,
|
|
367
|
+
compactionSummaries: this._compactionSummaries.length,
|
|
368
|
+
prefixCached: !!this._prefixCache,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Clear compaction summaries.
|
|
374
|
+
*/
|
|
375
|
+
clearCompactionSummaries() {
|
|
376
|
+
this._compactionSummaries = [];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Force reindex notes from DB.
|
|
381
|
+
*/
|
|
382
|
+
reindexNotes() {
|
|
383
|
+
this._notesIndexed = false;
|
|
384
|
+
this._bm25 = null;
|
|
385
|
+
this._ensureNotesIndex();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Internal ───────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
_cleanSystemPrompt(content) {
|
|
391
|
+
// Use stable prefix cache if available
|
|
392
|
+
const prefix = this._computeStablePrefix(content);
|
|
393
|
+
if (prefix) {
|
|
394
|
+
// Only clean the dynamic suffix
|
|
395
|
+
const suffix = content.slice(prefix.originalLength);
|
|
396
|
+
let cleanedSuffix = suffix;
|
|
397
|
+
for (const { pattern, replacement } of CLEAN_PATTERNS) {
|
|
398
|
+
cleanedSuffix = cleanedSuffix.replace(pattern, replacement);
|
|
399
|
+
}
|
|
400
|
+
return prefix.cleaned + cleanedSuffix;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let cleaned = content;
|
|
404
|
+
for (const { pattern, replacement } of CLEAN_PATTERNS) {
|
|
405
|
+
cleaned = cleaned.replace(pattern, replacement);
|
|
406
|
+
}
|
|
407
|
+
return cleaned;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Compute stable prefix — the portion of system prompt that doesn't change.
|
|
412
|
+
* Caches the cleaned prefix so subsequent calls only re-clean the dynamic tail.
|
|
413
|
+
*/
|
|
414
|
+
_computeStablePrefix(content) {
|
|
415
|
+
if (!content || content.length < 100) return null;
|
|
416
|
+
|
|
417
|
+
// Find the stable portion (before first dynamic pattern match)
|
|
418
|
+
let firstMatchIdx = content.length;
|
|
419
|
+
for (const { pattern } of CLEAN_PATTERNS) {
|
|
420
|
+
// Use non-global copy to get .index
|
|
421
|
+
const nonGlobal = new RegExp(
|
|
422
|
+
pattern.source,
|
|
423
|
+
pattern.flags.replace("g", ""),
|
|
424
|
+
);
|
|
425
|
+
const match = nonGlobal.exec(content);
|
|
426
|
+
if (match && match.index < firstMatchIdx) {
|
|
427
|
+
firstMatchIdx = match.index;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// If no dynamic content found, or prefix too short, skip caching
|
|
432
|
+
if (firstMatchIdx === content.length || firstMatchIdx < 50) return null;
|
|
433
|
+
|
|
434
|
+
const rawPrefix = content.slice(0, firstMatchIdx);
|
|
435
|
+
const hash = _deps
|
|
436
|
+
.createHash("sha256")
|
|
437
|
+
.update(rawPrefix)
|
|
438
|
+
.digest("hex")
|
|
439
|
+
.slice(0, 16);
|
|
440
|
+
|
|
441
|
+
if (this._prefixCache && this._prefixCache.hash === hash) {
|
|
442
|
+
return this._prefixCache;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Clean and cache the prefix (should be stable, no dynamic patterns)
|
|
446
|
+
let cleaned = rawPrefix;
|
|
447
|
+
for (const { pattern, replacement } of CLEAN_PATTERNS) {
|
|
448
|
+
cleaned = cleaned.replace(pattern, replacement);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this._prefixCache = { hash, cleaned, originalLength: firstMatchIdx };
|
|
452
|
+
return this._prefixCache;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_ensureNotesIndex() {
|
|
456
|
+
if (this._notesIndexed || !this.db) return;
|
|
457
|
+
this._notesIndexed = true;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const notes = this.db
|
|
461
|
+
.prepare("SELECT id, title, content FROM notes LIMIT 500")
|
|
462
|
+
.all();
|
|
463
|
+
|
|
464
|
+
if (notes && notes.length > 0) {
|
|
465
|
+
this._bm25 = new _deps.BM25Search();
|
|
466
|
+
this._bm25.indexDocuments(notes);
|
|
467
|
+
}
|
|
468
|
+
} catch (_err) {
|
|
469
|
+
// Notes table may not exist — skip
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|