metame-cli 1.4.34 → 1.5.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/README.md +136 -94
- package/index.js +312 -57
- package/package.json +8 -4
- package/scripts/agent-layer.js +320 -0
- package/scripts/daemon-admin-commands.js +328 -28
- package/scripts/daemon-agent-commands.js +145 -6
- package/scripts/daemon-agent-tools.js +163 -7
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-checkpoints.js +36 -7
- package/scripts/daemon-claude-engine.js +849 -358
- package/scripts/daemon-command-router.js +31 -10
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +328 -0
- package/scripts/daemon-exec-commands.js +15 -7
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-ops-commands.js +8 -6
- package/scripts/daemon-runtime-lifecycle.js +129 -5
- package/scripts/daemon-session-commands.js +60 -25
- package/scripts/daemon-session-store.js +121 -13
- package/scripts/daemon-task-scheduler.js +129 -49
- package/scripts/daemon-user-acl.js +35 -9
- package/scripts/daemon.js +268 -33
- package/scripts/distill.js +327 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +155 -0
- package/scripts/docs/pointer-map.md +110 -0
- package/scripts/feishu-adapter.js +42 -13
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +105 -6
- package/scripts/memory-nightly-reflect.js +199 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +24 -0
- package/scripts/providers.js +182 -22
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/telegram-adapter.js +12 -8
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
|
@@ -26,15 +26,17 @@ const REFLECT_LOG_FILE = path.join(METAME_DIR, 'memory_reflect_log.jsonl');
|
|
|
26
26
|
const MEMORY_DIR = path.join(HOME, '.metame', 'memory');
|
|
27
27
|
const DECISIONS_DIR = path.join(MEMORY_DIR, 'decisions');
|
|
28
28
|
const LESSONS_DIR = path.join(MEMORY_DIR, 'lessons');
|
|
29
|
+
const CAPSULES_DIR = path.join(MEMORY_DIR, 'capsules');
|
|
29
30
|
|
|
30
31
|
// Hot zone thresholds
|
|
31
32
|
const MIN_SEARCH_COUNT = 3;
|
|
32
33
|
const WINDOW_DAYS = 7;
|
|
33
34
|
const MAX_FACTS = 20;
|
|
34
35
|
const LOCK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
36
|
+
const EXCLUDED_RELATIONS = ['project_milestone', 'synthesized_insight', 'knowledge_capsule', 'bug_lesson'];
|
|
35
37
|
|
|
36
38
|
// Ensure output directories exist at startup
|
|
37
|
-
[MEMORY_DIR, DECISIONS_DIR, LESSONS_DIR].forEach(d => fs.mkdirSync(d, { recursive: true }));
|
|
39
|
+
[MEMORY_DIR, DECISIONS_DIR, LESSONS_DIR, CAPSULES_DIR].forEach(d => fs.mkdirSync(d, { recursive: true }));
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Load callHaiku + buildDistillEnv from deployed path, fallback to scripts dir.
|
|
@@ -107,18 +109,19 @@ function writeReflectLog(record) {
|
|
|
107
109
|
* Returns array of plain objects.
|
|
108
110
|
*/
|
|
109
111
|
function queryHotFacts(db) {
|
|
112
|
+
const relationPlaceholders = EXCLUDED_RELATIONS.map(() => '?').join(', ');
|
|
110
113
|
const stmt = db.prepare(`
|
|
111
|
-
SELECT entity, relation, value, confidence, search_count, created_at
|
|
114
|
+
SELECT id, entity, relation, value, confidence, search_count, created_at
|
|
112
115
|
FROM facts
|
|
113
116
|
WHERE search_count >= ${MIN_SEARCH_COUNT}
|
|
114
117
|
AND created_at >= datetime('now', '-${WINDOW_DAYS} days')
|
|
115
118
|
AND superseded_by IS NULL
|
|
116
119
|
AND (conflict_status IS NULL OR conflict_status = 'OK')
|
|
117
|
-
AND relation
|
|
120
|
+
AND relation NOT IN (${relationPlaceholders})
|
|
118
121
|
ORDER BY search_count DESC, created_at DESC
|
|
119
122
|
LIMIT ${MAX_FACTS}
|
|
120
123
|
`);
|
|
121
|
-
return stmt.all();
|
|
124
|
+
return stmt.all(...EXCLUDED_RELATIONS);
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
/**
|
|
@@ -142,6 +145,101 @@ ${sections}
|
|
|
142
145
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
function sanitizeSlug(input, fallback = 'capsule') {
|
|
149
|
+
const v = String(input || '')
|
|
150
|
+
.trim()
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.replace(/[^a-z0-9\u4e00-\u9fff_-]+/g, '-')
|
|
153
|
+
.replace(/-+/g, '-')
|
|
154
|
+
.replace(/^-|-$/g, '');
|
|
155
|
+
if (!v) return fallback;
|
|
156
|
+
return v.slice(0, 50);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function stripMd(text) {
|
|
160
|
+
return String(text || '').replace(/[#*_`>\[\]\(\)]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildSynthesizedFacts(today, decisions, lessons) {
|
|
164
|
+
const all = []
|
|
165
|
+
.concat(Array.isArray(decisions) ? decisions : [])
|
|
166
|
+
.concat(Array.isArray(lessons) ? lessons : []);
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const item of all) {
|
|
169
|
+
const title = String(item && item.title ? item.title : '').trim();
|
|
170
|
+
const content = String(item && item.content ? item.content : '').trim();
|
|
171
|
+
if (!title || !content) continue;
|
|
172
|
+
const value = stripMd(`${title}: ${content}`).slice(0, 280);
|
|
173
|
+
if (value.length < 20) continue;
|
|
174
|
+
out.push({
|
|
175
|
+
entity: `nightly.reflect.${today}`,
|
|
176
|
+
relation: 'synthesized_insight',
|
|
177
|
+
value,
|
|
178
|
+
confidence: 'high',
|
|
179
|
+
tags: ['nightly', 'reflection'],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function entityPrefix(entity) {
|
|
186
|
+
const src = String(entity || '').trim();
|
|
187
|
+
if (!src) return '';
|
|
188
|
+
const parts = src.split('.').map(s => s.trim()).filter(Boolean);
|
|
189
|
+
if (parts.length === 0) return '';
|
|
190
|
+
if (parts.length === 1) return parts[0];
|
|
191
|
+
return `${parts[0]}.${parts[1]}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collectCapsuleGroups(facts, minGroupSize = 3) {
|
|
195
|
+
const groups = new Map();
|
|
196
|
+
for (const fact of Array.isArray(facts) ? facts : []) {
|
|
197
|
+
const prefix = entityPrefix(fact && fact.entity);
|
|
198
|
+
if (!prefix) continue;
|
|
199
|
+
if (!groups.has(prefix)) groups.set(prefix, []);
|
|
200
|
+
groups.get(prefix).push(fact);
|
|
201
|
+
}
|
|
202
|
+
return [...groups.entries()]
|
|
203
|
+
.map(([prefix, items]) => ({ prefix, items }))
|
|
204
|
+
.filter(g => g.items.length >= minGroupSize)
|
|
205
|
+
.sort((a, b) => b.items.length - a.items.length);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseJsonFromLlm(raw) {
|
|
209
|
+
const text = String(raw || '');
|
|
210
|
+
const cleaned = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
211
|
+
if (!cleaned) return null;
|
|
212
|
+
try { return JSON.parse(cleaned); } catch { return null; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function writeCapsuleFile(filePath, capsule, facts, today, prefix) {
|
|
216
|
+
const related = Array.isArray(capsule.related_concepts) ? capsule.related_concepts.slice(0, 8) : [];
|
|
217
|
+
const supporting = Array.isArray(capsule.supporting_facts) ? capsule.supporting_facts.slice(0, 8) : [];
|
|
218
|
+
const content = `---
|
|
219
|
+
date: ${today}
|
|
220
|
+
source: nightly-reflect
|
|
221
|
+
type: knowledge-capsule
|
|
222
|
+
entity_prefix: ${prefix}
|
|
223
|
+
facts_analyzed: ${Array.isArray(facts) ? facts.length : 0}
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
# ${capsule.title}
|
|
227
|
+
|
|
228
|
+
## 核心结论
|
|
229
|
+
${capsule.core_conclusion}
|
|
230
|
+
|
|
231
|
+
## 适用场景
|
|
232
|
+
${capsule.applicable_scenarios}
|
|
233
|
+
|
|
234
|
+
## 关联概念
|
|
235
|
+
${related.length > 0 ? related.map(x => `- ${x}`).join('\n') : '- (none)'}
|
|
236
|
+
|
|
237
|
+
## 支撑事实
|
|
238
|
+
${supporting.length > 0 ? supporting.map(x => `- ${x}`).join('\n') : '- (none)'}
|
|
239
|
+
`;
|
|
240
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
241
|
+
}
|
|
242
|
+
|
|
145
243
|
/**
|
|
146
244
|
* Main nightly reflect run.
|
|
147
245
|
*/
|
|
@@ -238,12 +336,9 @@ Rules:
|
|
|
238
336
|
}
|
|
239
337
|
|
|
240
338
|
// Parse Haiku response
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
parsed = JSON.parse(cleaned);
|
|
245
|
-
} catch (e) {
|
|
246
|
-
console.log(`[NIGHTLY-REFLECT] Failed to parse Haiku output: ${e.message}`);
|
|
339
|
+
const parsed = parseJsonFromLlm(raw);
|
|
340
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
341
|
+
console.log('[NIGHTLY-REFLECT] Failed to parse Haiku output.');
|
|
247
342
|
writeReflectLog({ status: 'error', reason: 'parse_failed', facts_found: hotFacts.length });
|
|
248
343
|
return;
|
|
249
344
|
}
|
|
@@ -265,12 +360,92 @@ Rules:
|
|
|
265
360
|
console.log(`[NIGHTLY-REFLECT] Lessons written: ${lessonFile}`);
|
|
266
361
|
}
|
|
267
362
|
|
|
363
|
+
let synthesizedSaved = 0;
|
|
364
|
+
let capsulesWritten = 0;
|
|
365
|
+
let capsuleFactsSaved = 0;
|
|
366
|
+
let memory = null;
|
|
367
|
+
try {
|
|
368
|
+
try { memory = require('./memory'); } catch { /* optional */ }
|
|
369
|
+
|
|
370
|
+
// 3B: write distilled insights back into memory.db for closed-loop retrieval.
|
|
371
|
+
if (memory && typeof memory.saveFacts === 'function') {
|
|
372
|
+
const synthesizedFacts = buildSynthesizedFacts(today, decisions, lessons);
|
|
373
|
+
if (synthesizedFacts.length > 0) {
|
|
374
|
+
const writeRes = memory.saveFacts(`nightly-reflect-${today}`, '*', synthesizedFacts, { scope: '*' });
|
|
375
|
+
synthesizedSaved = Number(writeRes && writeRes.saved) || 0;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 3C: knowledge capsule aggregation by entity prefix.
|
|
380
|
+
const capsuleGroups = collectCapsuleGroups(hotFacts, 3).slice(0, 3);
|
|
381
|
+
for (const group of capsuleGroups) {
|
|
382
|
+
const groupFacts = group.items.map(f => ({
|
|
383
|
+
entity: f.entity,
|
|
384
|
+
relation: f.relation,
|
|
385
|
+
value: f.value,
|
|
386
|
+
search_count: f.search_count,
|
|
387
|
+
}));
|
|
388
|
+
const capsulePrompt = `你是知识胶囊生成器。请将同一主题下的事实聚合成结构化胶囊。
|
|
389
|
+
|
|
390
|
+
entity_prefix: ${group.prefix}
|
|
391
|
+
facts(json): ${JSON.stringify(groupFacts, null, 2).slice(0, 5000)}
|
|
392
|
+
|
|
393
|
+
输出 JSON:
|
|
394
|
+
{
|
|
395
|
+
"title":"标题",
|
|
396
|
+
"core_conclusion":"一句核心结论",
|
|
397
|
+
"applicable_scenarios":"适用场景(1-2句)",
|
|
398
|
+
"related_concepts":["概念1","概念2"],
|
|
399
|
+
"supporting_facts":["支撑点1","支撑点2"]
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
规则:
|
|
403
|
+
- 只基于输入事实,不虚构
|
|
404
|
+
- 每个字段简洁具体
|
|
405
|
+
- 仅输出 JSON`;
|
|
406
|
+
|
|
407
|
+
let capsule = null;
|
|
408
|
+
try {
|
|
409
|
+
const rawCapsule = await Promise.race([
|
|
410
|
+
callHaiku(capsulePrompt, distillEnv, 60000),
|
|
411
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 65000)),
|
|
412
|
+
]);
|
|
413
|
+
capsule = parseJsonFromLlm(rawCapsule);
|
|
414
|
+
} catch { /* non-fatal */ }
|
|
415
|
+
if (!capsule || !capsule.title || !capsule.core_conclusion || !capsule.applicable_scenarios) continue;
|
|
416
|
+
|
|
417
|
+
const capsuleSlug = sanitizeSlug(group.prefix.replace(/\./g, '-'), 'capsule');
|
|
418
|
+
const capsuleFile = path.join(CAPSULES_DIR, `${capsuleSlug}-${today}.md`);
|
|
419
|
+
writeCapsuleFile(capsuleFile, capsule, group.items, today, group.prefix);
|
|
420
|
+
capsulesWritten++;
|
|
421
|
+
|
|
422
|
+
if (memory && typeof memory.saveFacts === 'function') {
|
|
423
|
+
const capsuleValue = stripMd(`${capsule.title}: ${capsule.core_conclusion}`).slice(0, 280);
|
|
424
|
+
if (capsuleValue.length >= 20) {
|
|
425
|
+
const saveCapsule = memory.saveFacts(`capsule-${today}-${capsuleSlug}`, '*', [{
|
|
426
|
+
entity: `capsule.${group.prefix.replace(/\./g, '_')}`,
|
|
427
|
+
relation: 'knowledge_capsule',
|
|
428
|
+
value: capsuleValue,
|
|
429
|
+
confidence: 'high',
|
|
430
|
+
tags: ['capsule'],
|
|
431
|
+
}], { scope: '*' });
|
|
432
|
+
capsuleFactsSaved += Number(saveCapsule && saveCapsule.saved) || 0;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} finally {
|
|
437
|
+
try { if (memory && typeof memory.close === 'function') memory.close(); } catch { /* non-fatal */ }
|
|
438
|
+
}
|
|
439
|
+
|
|
268
440
|
// Write audit log
|
|
269
441
|
writeReflectLog({
|
|
270
442
|
status: 'success',
|
|
271
443
|
facts_analyzed: hotFacts.length,
|
|
272
444
|
decisions_written: decisions.length,
|
|
273
445
|
lessons_written: lessons.length,
|
|
446
|
+
synthesized_insights_saved: synthesizedSaved,
|
|
447
|
+
capsules_written: capsulesWritten,
|
|
448
|
+
capsule_facts_saved: capsuleFactsSaved,
|
|
274
449
|
decision_file: decisions.length > 0 ? decisionFile : null,
|
|
275
450
|
lesson_file: lessons.length > 0 ? lessonFile : null,
|
|
276
451
|
});
|
|
@@ -290,10 +465,23 @@ Rules:
|
|
|
290
465
|
if (require.main === module) {
|
|
291
466
|
run().then(() => {
|
|
292
467
|
console.log('✅ nightly-reflect complete');
|
|
468
|
+
// Report estimated token usage for daemon budget tracking
|
|
469
|
+
// ~5k tokens per reflection + capsule generation
|
|
470
|
+
console.log('__TOKENS__:5000');
|
|
293
471
|
}).catch(e => {
|
|
294
472
|
console.error(`[NIGHTLY-REFLECT] Fatal: ${e.message}`);
|
|
295
473
|
process.exit(1);
|
|
296
474
|
});
|
|
297
475
|
}
|
|
298
476
|
|
|
299
|
-
module.exports = {
|
|
477
|
+
module.exports = {
|
|
478
|
+
run,
|
|
479
|
+
_private: {
|
|
480
|
+
queryHotFacts,
|
|
481
|
+
buildSynthesizedFacts,
|
|
482
|
+
collectCapsuleGroups,
|
|
483
|
+
entityPrefix,
|
|
484
|
+
parseJsonFromLlm,
|
|
485
|
+
EXCLUDED_RELATIONS,
|
|
486
|
+
},
|
|
487
|
+
};
|
package/scripts/memory.js
CHANGED
|
@@ -122,6 +122,17 @@ function getDb() {
|
|
|
122
122
|
)
|
|
123
123
|
`);
|
|
124
124
|
|
|
125
|
+
// Optional concept label side-table (non-invasive, no ALTER on facts schema)
|
|
126
|
+
_db.exec(`
|
|
127
|
+
CREATE TABLE IF NOT EXISTS fact_labels (
|
|
128
|
+
fact_id TEXT NOT NULL REFERENCES facts(id) ON DELETE CASCADE,
|
|
129
|
+
label TEXT NOT NULL,
|
|
130
|
+
domain TEXT,
|
|
131
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
132
|
+
PRIMARY KEY (fact_id, label)
|
|
133
|
+
)
|
|
134
|
+
`);
|
|
135
|
+
|
|
125
136
|
// FTS5 index for facts (separate from sessions_fts, zero compatibility risk)
|
|
126
137
|
try {
|
|
127
138
|
_db.exec(`
|
|
@@ -159,6 +170,8 @@ function getDb() {
|
|
|
159
170
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch {}
|
|
160
171
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity_relation ON facts(entity, relation)'); } catch {}
|
|
161
172
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch {}
|
|
173
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_label ON fact_labels(label)'); } catch {}
|
|
174
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_domain ON fact_labels(domain)'); } catch {}
|
|
162
175
|
|
|
163
176
|
// Backward-compatible migration for old DBs without `scope`
|
|
164
177
|
try { _db.exec('ALTER TABLE facts ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
|
|
@@ -369,7 +382,43 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
|
369
382
|
|
|
370
383
|
if (conflicts > 0) log('WARN', `[MEMORY] ${conflicts} conflict(s) detected`);
|
|
371
384
|
|
|
372
|
-
return { saved, skipped, superseded, conflicts };
|
|
385
|
+
return { saved, skipped, superseded, conflicts, savedFacts };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Save concept labels for facts (side-table).
|
|
390
|
+
*
|
|
391
|
+
* @param {Array<{fact_id:string,label:string,domain?:string}>} rows
|
|
392
|
+
* @returns {{ saved: number, skipped: number }}
|
|
393
|
+
*/
|
|
394
|
+
function saveFactLabels(rows) {
|
|
395
|
+
if (!Array.isArray(rows) || rows.length === 0) return { saved: 0, skipped: 0 };
|
|
396
|
+
const db = getDb();
|
|
397
|
+
const upsert = db.prepare(`
|
|
398
|
+
INSERT INTO fact_labels (fact_id, label, domain)
|
|
399
|
+
VALUES (?, ?, ?)
|
|
400
|
+
ON CONFLICT(fact_id, label) DO UPDATE SET
|
|
401
|
+
domain = COALESCE(excluded.domain, fact_labels.domain)
|
|
402
|
+
`);
|
|
403
|
+
|
|
404
|
+
let saved = 0;
|
|
405
|
+
let skipped = 0;
|
|
406
|
+
for (const row of rows) {
|
|
407
|
+
const factId = String(row && row.fact_id ? row.fact_id : '').trim();
|
|
408
|
+
const label = String(row && row.label ? row.label : '').trim();
|
|
409
|
+
const domainRaw = row && row.domain != null ? String(row.domain).trim() : '';
|
|
410
|
+
const domain = domainRaw || null;
|
|
411
|
+
if (!factId || !label) { skipped++; continue; }
|
|
412
|
+
if (label.length > 60) { skipped++; continue; }
|
|
413
|
+
if (domain && domain.length > 60) { skipped++; continue; }
|
|
414
|
+
try {
|
|
415
|
+
upsert.run(factId, label, domain);
|
|
416
|
+
saved++;
|
|
417
|
+
} catch {
|
|
418
|
+
skipped++;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { saved, skipped };
|
|
373
422
|
}
|
|
374
423
|
|
|
375
424
|
/**
|
|
@@ -613,12 +662,46 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
613
662
|
}
|
|
614
663
|
const ftsResults = db.prepare(sql).all(...params);
|
|
615
664
|
if (ftsResults.length > 0) {
|
|
665
|
+
// Supplement with fact_labels matches (concepts written by memory-extract).
|
|
666
|
+
const ftsIds = new Set(ftsResults.map(r => r.id));
|
|
667
|
+
const remaining = limit - ftsResults.length;
|
|
668
|
+
if (remaining > 0) {
|
|
669
|
+
try {
|
|
670
|
+
const labelLike = '%' + query.trim() + '%';
|
|
671
|
+
let labelSql = `
|
|
672
|
+
SELECT DISTINCT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at
|
|
673
|
+
FROM fact_labels fl JOIN facts f ON f.id = fl.fact_id
|
|
674
|
+
WHERE fl.label LIKE ?
|
|
675
|
+
AND f.superseded_by IS NULL
|
|
676
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))`;
|
|
677
|
+
const labelParams = [labelLike];
|
|
678
|
+
if (scope && project) {
|
|
679
|
+
labelSql += ` AND ((f.scope = ? OR f.scope = '*') OR (f.scope IS NULL AND (f.project = ? OR f.project = '*')))`;
|
|
680
|
+
labelParams.push(scope, project);
|
|
681
|
+
} else if (scope) {
|
|
682
|
+
labelSql += ` AND (f.scope = ? OR f.scope = '*')`;
|
|
683
|
+
labelParams.push(scope);
|
|
684
|
+
} else if (project) {
|
|
685
|
+
labelSql += ` AND (f.project = ? OR f.project = '*')`;
|
|
686
|
+
labelParams.push(project);
|
|
687
|
+
}
|
|
688
|
+
labelSql += ` LIMIT ?`;
|
|
689
|
+
labelParams.push(remaining + ftsResults.length);
|
|
690
|
+
const labelRows = db.prepare(labelSql).all(...labelParams);
|
|
691
|
+
for (const row of labelRows) {
|
|
692
|
+
if (!ftsIds.has(row.id) && ftsResults.length < limit) {
|
|
693
|
+
ftsIds.add(row.id);
|
|
694
|
+
ftsResults.push(row);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} catch { /* fact_labels table may not exist yet */ }
|
|
698
|
+
}
|
|
616
699
|
_trackSearch(ftsResults.map(r => r.id));
|
|
617
700
|
return ftsResults;
|
|
618
701
|
}
|
|
619
702
|
} catch { /* FTS error, fall through */ }
|
|
620
703
|
|
|
621
|
-
// LIKE fallback
|
|
704
|
+
// LIKE fallback (also check fact_labels)
|
|
622
705
|
const like = '%' + query.trim() + '%';
|
|
623
706
|
const likeSql = scope && project
|
|
624
707
|
? `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
@@ -651,6 +734,39 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
651
734
|
: project
|
|
652
735
|
? db.prepare(likeSql).all(like, like, like, project, limit)
|
|
653
736
|
: db.prepare(likeSql).all(like, like, like, limit);
|
|
737
|
+
// Supplement LIKE results with fact_labels matches.
|
|
738
|
+
if (likeResults.length < limit) {
|
|
739
|
+
try {
|
|
740
|
+
const labelLike = '%' + query.trim() + '%';
|
|
741
|
+
const likeIds = new Set(likeResults.map(r => r.id));
|
|
742
|
+
let labelSql2 = `
|
|
743
|
+
SELECT DISTINCT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at
|
|
744
|
+
FROM fact_labels fl JOIN facts f ON f.id = fl.fact_id
|
|
745
|
+
WHERE fl.label LIKE ?
|
|
746
|
+
AND f.superseded_by IS NULL
|
|
747
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))`;
|
|
748
|
+
const labelParams2 = [labelLike];
|
|
749
|
+
if (scope && project) {
|
|
750
|
+
labelSql2 += ` AND ((f.scope = ? OR f.scope = '*') OR (f.scope IS NULL AND (f.project = ? OR f.project = '*')))`;
|
|
751
|
+
labelParams2.push(scope, project);
|
|
752
|
+
} else if (scope) {
|
|
753
|
+
labelSql2 += ` AND (f.scope = ? OR f.scope = '*')`;
|
|
754
|
+
labelParams2.push(scope);
|
|
755
|
+
} else if (project) {
|
|
756
|
+
labelSql2 += ` AND (f.project = ? OR f.project = '*')`;
|
|
757
|
+
labelParams2.push(project);
|
|
758
|
+
}
|
|
759
|
+
labelSql2 += ` LIMIT ?`;
|
|
760
|
+
labelParams2.push(limit);
|
|
761
|
+
const labelRows = db.prepare(labelSql2).all(...labelParams2);
|
|
762
|
+
for (const row of labelRows) {
|
|
763
|
+
if (!likeIds.has(row.id) && likeResults.length < limit) {
|
|
764
|
+
likeIds.add(row.id);
|
|
765
|
+
likeResults.push(row);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
} catch { /* fact_labels table may not exist yet */ }
|
|
769
|
+
}
|
|
654
770
|
if (likeResults.length > 0) _trackSearch(likeResults.map(r => r.id));
|
|
655
771
|
return likeResults;
|
|
656
772
|
}
|
|
@@ -839,4 +955,19 @@ function forceClose() {
|
|
|
839
955
|
if (_db) { _db.close(); _db = null; }
|
|
840
956
|
}
|
|
841
957
|
|
|
842
|
-
module.exports = {
|
|
958
|
+
module.exports = {
|
|
959
|
+
saveSession,
|
|
960
|
+
saveFacts,
|
|
961
|
+
saveFactLabels,
|
|
962
|
+
searchFacts,
|
|
963
|
+
searchFactsAsync,
|
|
964
|
+
searchSessions,
|
|
965
|
+
recentSessions,
|
|
966
|
+
getSession,
|
|
967
|
+
stats,
|
|
968
|
+
acquire,
|
|
969
|
+
release,
|
|
970
|
+
close,
|
|
971
|
+
forceClose,
|
|
972
|
+
DB_PATH,
|
|
973
|
+
};
|