metame-cli 1.4.18 → 1.4.20
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 +124 -38
- package/index.js +39 -1
- package/package.json +2 -2
- package/scripts/daemon-admin-commands.js +86 -4
- package/scripts/daemon-agent-commands.js +91 -62
- package/scripts/daemon-agent-tools.js +49 -12
- package/scripts/daemon-bridges.js +26 -6
- package/scripts/daemon-claude-engine.js +111 -32
- package/scripts/daemon-command-router.js +32 -15
- package/scripts/daemon-default.yaml +18 -0
- package/scripts/daemon-exec-commands.js +6 -12
- package/scripts/daemon-file-browser.js +6 -5
- package/scripts/daemon-runtime-lifecycle.js +19 -5
- package/scripts/daemon-session-store.js +176 -41
- package/scripts/daemon-task-scheduler.js +30 -29
- package/scripts/daemon-user-acl.js +399 -0
- package/scripts/daemon.js +43 -6
- package/scripts/distill.js +11 -12
- package/scripts/memory-gc.js +239 -0
- package/scripts/memory-index.js +103 -0
- package/scripts/memory-nightly-reflect.js +299 -0
- package/scripts/memory-write.js +192 -0
- package/scripts/memory.js +144 -6
- package/scripts/schema.js +30 -9
- package/scripts/self-reflect.js +121 -5
- package/scripts/session-analytics.js +9 -10
- package/scripts/task-board.js +9 -3
- package/scripts/telegram-adapter.js +77 -9
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* memory-write.js — Active memory injection CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node memory-write.js "entity" "relation" "value"
|
|
7
|
+
* node memory-write.js "entity" "relation" "value" --confidence high --project metame --tags "tag1,tag2"
|
|
8
|
+
* node memory-write.js --help
|
|
9
|
+
*
|
|
10
|
+
* Reuses memory.js saveFacts() and acquire/release pattern. Zero new dependencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
// ── Legal relation types ──────────────────────────────────────────────────────
|
|
19
|
+
const VALID_RELATIONS = new Set([
|
|
20
|
+
'tech_decision',
|
|
21
|
+
'bug_lesson',
|
|
22
|
+
'arch_convention',
|
|
23
|
+
'config_fact',
|
|
24
|
+
'config_change',
|
|
25
|
+
'user_pref',
|
|
26
|
+
'workflow_rule',
|
|
27
|
+
'project_milestone',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// ── Entity whitelist (no dot required for these) ──────────────────────────────
|
|
31
|
+
const ENTITY_WHITELIST = new Set(['王总', 'system', 'user', 'MetaMe']);
|
|
32
|
+
|
|
33
|
+
// ── Help text ─────────────────────────────────────────────────────────────────
|
|
34
|
+
const HELP = `
|
|
35
|
+
memory-write.js — 主动写入事实到 memory.db
|
|
36
|
+
|
|
37
|
+
用法:
|
|
38
|
+
node memory-write.js <entity> <relation> <value> [options]
|
|
39
|
+
|
|
40
|
+
参数:
|
|
41
|
+
entity 知识主体,须含点号(如 MetaMe.daemon)或在白名单(王总/system/user/MetaMe)
|
|
42
|
+
relation 关系类型,合法值:
|
|
43
|
+
${[...VALID_RELATIONS].join(', ')}
|
|
44
|
+
value 事实内容,20-300 字符
|
|
45
|
+
|
|
46
|
+
选项:
|
|
47
|
+
--confidence <level> high | medium | low (默认 medium)
|
|
48
|
+
--project <key> 项目标识(默认从 cwd 推断,推断不到则 '*')
|
|
49
|
+
--tags <tag1,tag2> 最多 3 个标签,逗号分隔
|
|
50
|
+
--help 显示此帮助
|
|
51
|
+
|
|
52
|
+
示例:
|
|
53
|
+
node memory-write.js "MetaMe.daemon" "bug_lesson" "修复X前必须先Y,否则Z会挂"
|
|
54
|
+
node memory-write.js "MetaMe.bridge" "tech_decision" "飞书回调走 3000 端口,nginx 转发" --confidence high --project metame
|
|
55
|
+
`.trim();
|
|
56
|
+
|
|
57
|
+
// ── Project inference ─────────────────────────────────────────────────────────
|
|
58
|
+
function inferProject(cwd) {
|
|
59
|
+
if (!cwd) return '*';
|
|
60
|
+
const known = { 'MetaMe': 'metame', 'metame-desktop': 'desktop' };
|
|
61
|
+
for (const [dir, key] of Object.entries(known)) {
|
|
62
|
+
if (cwd.includes(dir)) return key;
|
|
63
|
+
}
|
|
64
|
+
return '*';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Argv parser (zero deps) ───────────────────────────────────────────────────
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const positional = [];
|
|
70
|
+
const opts = {};
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < argv.length) {
|
|
73
|
+
const a = argv[i];
|
|
74
|
+
if (a === '--help' || a === '-h') { opts.help = true; i++; continue; }
|
|
75
|
+
if (a.startsWith('--')) {
|
|
76
|
+
const key = a.slice(2);
|
|
77
|
+
const val = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : null;
|
|
78
|
+
if (val !== null) { opts[key] = val; i += 2; } else { opts[key] = true; i++; }
|
|
79
|
+
} else {
|
|
80
|
+
positional.push(a);
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { positional, opts };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Validation ────────────────────────────────────────────────────────────────
|
|
88
|
+
function validateEntity(entity) {
|
|
89
|
+
if (!entity) return 'entity 不能为空';
|
|
90
|
+
if (ENTITY_WHITELIST.has(entity)) return null;
|
|
91
|
+
if (!entity.includes('.')) return `entity 格式不合法:须含点号(如 MetaMe.daemon)或为白名单值(${[...ENTITY_WHITELIST].join('/')})`;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateRelation(relation) {
|
|
96
|
+
if (!relation) return 'relation 不能为空';
|
|
97
|
+
if (!VALID_RELATIONS.has(relation)) {
|
|
98
|
+
return `relation 不合法:"${relation}"\n合法值: ${[...VALID_RELATIONS].join(', ')}`;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateValue(value) {
|
|
104
|
+
if (!value) return 'value 不能为空';
|
|
105
|
+
if (value.length < 20) return `value 太短(${value.length} 字符),最少 20 字符`;
|
|
106
|
+
if (value.length > 300) return `value 太长(${value.length} 字符),最多 300 字符`;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateConfidence(conf) {
|
|
111
|
+
const valid = ['high', 'medium', 'low'];
|
|
112
|
+
if (!valid.includes(conf)) return `confidence 不合法:"${conf}",合法值: high | medium | low`;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Resolve memory.js (supports both dev and deployed paths) ──────────────────
|
|
117
|
+
function resolveMemory() {
|
|
118
|
+
const candidates = [
|
|
119
|
+
path.join(os.homedir(), '.metame', 'memory.js'),
|
|
120
|
+
path.join(__dirname, 'memory.js'),
|
|
121
|
+
];
|
|
122
|
+
for (const p of candidates) {
|
|
123
|
+
try { require.resolve(p); return p; } catch { }
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
129
|
+
function main() {
|
|
130
|
+
const { positional, opts } = parseArgs(process.argv.slice(2));
|
|
131
|
+
|
|
132
|
+
if (opts.help) {
|
|
133
|
+
console.log(HELP);
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const [entity, relation, value] = positional;
|
|
138
|
+
const confidence = opts.confidence || 'medium';
|
|
139
|
+
const project = opts.project || process.env.METAME_PROJECT || inferProject(process.cwd());
|
|
140
|
+
const tagsRaw = opts.tags || '';
|
|
141
|
+
const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean).slice(0, 3) : [];
|
|
142
|
+
|
|
143
|
+
// Validate
|
|
144
|
+
const errors = [
|
|
145
|
+
validateEntity(entity),
|
|
146
|
+
validateRelation(relation),
|
|
147
|
+
validateValue(value),
|
|
148
|
+
opts.confidence ? validateConfidence(confidence) : null,
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
|
|
151
|
+
if (errors.length) {
|
|
152
|
+
for (const e of errors) console.error('错误: ' + e);
|
|
153
|
+
console.error('\n运行 node memory-write.js --help 查看用法');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Resolve memory module
|
|
158
|
+
const memoryPath = resolveMemory();
|
|
159
|
+
if (!memoryPath) {
|
|
160
|
+
console.error('错误: 找不到 memory.js,请确认 MetaMe 已部署');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const memory = require(memoryPath);
|
|
165
|
+
const sessionId = 'manual-' + Date.now();
|
|
166
|
+
|
|
167
|
+
if (typeof memory.acquire === 'function') memory.acquire();
|
|
168
|
+
try {
|
|
169
|
+
const result = memory.saveFacts(sessionId, project, [
|
|
170
|
+
{ entity, relation, value, confidence, tags, source_type: 'manual' },
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
if (result.saved > 0) {
|
|
174
|
+
const preview = value.slice(0, 30) + (value.length > 30 ? '...' : '');
|
|
175
|
+
console.log(`✓ 已保存 [${relation}] ${entity}: "${preview}"`);
|
|
176
|
+
if (result.superseded > 0) {
|
|
177
|
+
console.log(` (已将 ${result.superseded} 条旧记录标记为 superseded)`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// skipped: duplicate or validation filtered by saveFacts
|
|
181
|
+
console.log(`⚠ 未保存(可能是重复内容)。已跳过: ${result.skipped}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error('错误: 写入失败 —', err.message);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
} finally {
|
|
188
|
+
try { if (typeof memory.release === 'function') memory.release(); } catch { }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main();
|
package/scripts/memory.js
CHANGED
|
@@ -25,8 +25,17 @@ const fs = require('fs');
|
|
|
25
25
|
|
|
26
26
|
const DB_PATH = path.join(os.homedir(), '.metame', 'memory.db');
|
|
27
27
|
|
|
28
|
+
/** Minimal structured logger. level: 'INFO' | 'WARN' | 'ERROR' */
|
|
29
|
+
function log(level, msg) {
|
|
30
|
+
const ts = new Date().toISOString();
|
|
31
|
+
process.stderr.write(`${ts} [${level}] ${msg}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
// Lazy-init: only open DB when first called
|
|
29
35
|
let _db = null;
|
|
36
|
+
// Counts external callers that have called acquire() but not yet release().
|
|
37
|
+
// Internal helpers (getDb, _trackSearch, etc.) do NOT affect this counter.
|
|
38
|
+
let _refCount = 0;
|
|
30
39
|
|
|
31
40
|
function getDb() {
|
|
32
41
|
if (_db) return _db;
|
|
@@ -164,6 +173,9 @@ function getDb() {
|
|
|
164
173
|
// One-time migration: copy recall_count → search_count for existing rows
|
|
165
174
|
try { _db.exec('UPDATE facts SET search_count = recall_count WHERE recall_count > 0 AND search_count = 0'); } catch {}
|
|
166
175
|
|
|
176
|
+
// conflict_status: 'OK' (default) | 'CONFLICT' — set by _detectConflict for non-stateful relations
|
|
177
|
+
try { _db.exec("ALTER TABLE facts ADD COLUMN conflict_status TEXT NOT NULL DEFAULT 'OK'"); } catch {}
|
|
178
|
+
|
|
167
179
|
return _db;
|
|
168
180
|
}
|
|
169
181
|
|
|
@@ -260,13 +272,14 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
|
260
272
|
|
|
261
273
|
const insert = db.prepare(`
|
|
262
274
|
INSERT INTO facts (id, entity, relation, value, confidence, source_type, source_id, project, scope, tags, created_at, updated_at)
|
|
263
|
-
VALUES (?, ?, ?, ?, ?,
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
264
276
|
ON CONFLICT(id) DO NOTHING
|
|
265
277
|
`);
|
|
266
278
|
|
|
267
279
|
let saved = 0;
|
|
268
280
|
let skipped = 0;
|
|
269
281
|
let superseded = 0;
|
|
282
|
+
let conflicts = 0;
|
|
270
283
|
const savedFacts = [];
|
|
271
284
|
const batchDedup = new Set();
|
|
272
285
|
|
|
@@ -287,8 +300,9 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
|
287
300
|
const id = `f-${sessionId.slice(0, 8)}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
288
301
|
const tags = JSON.stringify(Array.isArray(f.tags) ? f.tags.slice(0, 3) : []);
|
|
289
302
|
try {
|
|
303
|
+
const sourceType = f.source_type || 'session';
|
|
290
304
|
insert.run(id, f.entity, f.relation, f.value.slice(0, 300),
|
|
291
|
-
f.confidence || 'medium', sessionId, normalizedProject, normalizedScope, tags);
|
|
305
|
+
f.confidence || 'medium', sourceType, sessionId, normalizedProject, normalizedScope, tags);
|
|
292
306
|
batchDedup.add(dedupKey);
|
|
293
307
|
savedFacts.push({ id, entity: f.entity, relation: f.relation, value: f.value,
|
|
294
308
|
project: normalizedProject, scope: normalizedScope, tags: f.tags || [], created_at: new Date().toISOString() });
|
|
@@ -328,6 +342,20 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
|
328
342
|
if (changes > 0) {
|
|
329
343
|
_logSupersede(toSupersede, id, f.entity, f.relation, f.value, sessionId);
|
|
330
344
|
}
|
|
345
|
+
} else {
|
|
346
|
+
// Conflict detection for non-stateful relations
|
|
347
|
+
let whereSql = '';
|
|
348
|
+
let filterParams = [];
|
|
349
|
+
if (normalizedScope === '*') {
|
|
350
|
+
whereSql = `((scope = '*') OR (scope IS NULL AND project = '*'))`;
|
|
351
|
+
} else if (normalizedScope) {
|
|
352
|
+
whereSql = `((scope = ?) OR (scope IS NULL AND project = ?))`;
|
|
353
|
+
filterParams = [normalizedScope, normalizedProject];
|
|
354
|
+
} else {
|
|
355
|
+
whereSql = `(project IN (?, '*'))`;
|
|
356
|
+
filterParams = [normalizedProject];
|
|
357
|
+
}
|
|
358
|
+
conflicts += _detectConflict(db, f, id, whereSql, filterParams, sessionId);
|
|
331
359
|
}
|
|
332
360
|
} catch { skipped++; }
|
|
333
361
|
}
|
|
@@ -339,7 +367,9 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
|
339
367
|
if (qmdClient) qmdClient.upsertFacts(savedFacts);
|
|
340
368
|
}
|
|
341
369
|
|
|
342
|
-
|
|
370
|
+
if (conflicts > 0) log('WARN', `[MEMORY] ${conflicts} conflict(s) detected`);
|
|
371
|
+
|
|
372
|
+
return { saved, skipped, superseded, conflicts };
|
|
343
373
|
}
|
|
344
374
|
|
|
345
375
|
/**
|
|
@@ -361,6 +391,7 @@ function _trackSearch(ids) {
|
|
|
361
391
|
}
|
|
362
392
|
|
|
363
393
|
const SUPERSEDE_LOG = path.join(os.homedir(), '.metame', 'memory_supersede_log.jsonl');
|
|
394
|
+
const CONFLICT_LOG = path.join(os.homedir(), '.metame', 'memory_conflict_log.jsonl');
|
|
364
395
|
|
|
365
396
|
/**
|
|
366
397
|
* Append supersede operations to audit log (append-only, never mutated).
|
|
@@ -383,6 +414,77 @@ function _logSupersede(oldFacts, newId, entity, relation, newValue, sessionId) {
|
|
|
383
414
|
} catch { /* non-fatal */ }
|
|
384
415
|
}
|
|
385
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Detect value conflicts for non-stateful facts.
|
|
419
|
+
*
|
|
420
|
+
* When a new fact (entity, relation) already has an active record whose value
|
|
421
|
+
* differs significantly from the incoming value, both are flagged CONFLICT.
|
|
422
|
+
* "Significant difference" = trimmed values are not equal AND neither contains
|
|
423
|
+
* the other as a substring (handles minor rewording and prefix matches).
|
|
424
|
+
*
|
|
425
|
+
* @param {object} db - DatabaseSync instance
|
|
426
|
+
* @param {object} fact - The newly-inserted fact { entity, relation, value }
|
|
427
|
+
* @param {string} newId - Row ID of the newly-inserted fact
|
|
428
|
+
* @param {string} whereSql - Scope WHERE clause (reused from saveFacts)
|
|
429
|
+
* @param {Array} filterParams - Bind params for whereSql
|
|
430
|
+
* @param {string} sessionId - Source session ID (for audit log)
|
|
431
|
+
* @returns {number} Number of conflicts detected (0 or more)
|
|
432
|
+
*/
|
|
433
|
+
function _detectConflict(db, fact, newId, whereSql, filterParams, sessionId) {
|
|
434
|
+
try {
|
|
435
|
+
const existing = db.prepare(
|
|
436
|
+
`SELECT id, value FROM facts
|
|
437
|
+
WHERE entity = ? AND relation = ? AND id != ? AND superseded_by IS NULL
|
|
438
|
+
AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
439
|
+
AND ${whereSql}`
|
|
440
|
+
).all(fact.entity, fact.relation, newId, ...filterParams);
|
|
441
|
+
|
|
442
|
+
if (existing.length === 0) return 0;
|
|
443
|
+
|
|
444
|
+
const newVal = fact.value.trim();
|
|
445
|
+
let conflictCount = 0;
|
|
446
|
+
|
|
447
|
+
const conflicting = [];
|
|
448
|
+
for (const row of existing) {
|
|
449
|
+
const oldVal = row.value.trim();
|
|
450
|
+
// Skip if values are equivalent or one contains the other
|
|
451
|
+
if (oldVal === newVal) continue;
|
|
452
|
+
if (oldVal.includes(newVal) || newVal.includes(oldVal)) continue;
|
|
453
|
+
|
|
454
|
+
// Mark existing record as CONFLICT
|
|
455
|
+
db.prepare(
|
|
456
|
+
`UPDATE facts SET conflict_status = 'CONFLICT', updated_at = datetime('now') WHERE id = ?`
|
|
457
|
+
).run(row.id);
|
|
458
|
+
|
|
459
|
+
conflicting.push({ id: row.id, value: row.value.slice(0, 80) });
|
|
460
|
+
conflictCount++;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (conflictCount > 0) {
|
|
464
|
+
// Mark the new fact as CONFLICT too
|
|
465
|
+
db.prepare(
|
|
466
|
+
`UPDATE facts SET conflict_status = 'CONFLICT', updated_at = datetime('now') WHERE id = ?`
|
|
467
|
+
).run(newId);
|
|
468
|
+
|
|
469
|
+
// Audit log (append-only, never mutated)
|
|
470
|
+
try {
|
|
471
|
+
const entry = {
|
|
472
|
+
ts: new Date().toISOString(),
|
|
473
|
+
entity: fact.entity,
|
|
474
|
+
relation: fact.relation,
|
|
475
|
+
new_id: newId,
|
|
476
|
+
new_value: fact.value.slice(0, 80),
|
|
477
|
+
session_id: sessionId,
|
|
478
|
+
conflicting,
|
|
479
|
+
};
|
|
480
|
+
fs.appendFileSync(CONFLICT_LOG, JSON.stringify(entry) + '\n', 'utf8');
|
|
481
|
+
} catch { /* non-fatal */ }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return conflictCount;
|
|
485
|
+
} catch { return 0; }
|
|
486
|
+
}
|
|
487
|
+
|
|
386
488
|
/**
|
|
387
489
|
* Scope filter semantics (new + legacy):
|
|
388
490
|
* - New rows: prefer `scope` exact match or global scope '*'
|
|
@@ -426,7 +528,8 @@ async function searchFactsAsync(query, { limit = 5, project = null, scope = null
|
|
|
426
528
|
const placeholders = ids.map(() => '?').join(',');
|
|
427
529
|
let rows = db.prepare(
|
|
428
530
|
`SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
429
|
-
FROM facts WHERE id IN (${placeholders}) AND superseded_by IS NULL
|
|
531
|
+
FROM facts WHERE id IN (${placeholders}) AND superseded_by IS NULL
|
|
532
|
+
AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))`
|
|
430
533
|
).all(...ids);
|
|
431
534
|
|
|
432
535
|
// Apply project/scope filter
|
|
@@ -476,6 +579,7 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
476
579
|
WHERE facts_fts MATCH ?
|
|
477
580
|
AND ((f.scope = ? OR f.scope = '*') OR (f.scope IS NULL AND (f.project = ? OR f.project = '*')))
|
|
478
581
|
AND f.superseded_by IS NULL
|
|
582
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
479
583
|
ORDER BY rank LIMIT ?
|
|
480
584
|
`;
|
|
481
585
|
params = [sanitized, scope, project, limit];
|
|
@@ -484,6 +588,7 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
484
588
|
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at, rank
|
|
485
589
|
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
486
590
|
WHERE facts_fts MATCH ? AND (f.scope = ? OR f.scope = '*') AND f.superseded_by IS NULL
|
|
591
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
487
592
|
ORDER BY rank LIMIT ?
|
|
488
593
|
`;
|
|
489
594
|
params = [sanitized, scope, limit];
|
|
@@ -492,6 +597,7 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
492
597
|
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at, rank
|
|
493
598
|
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
494
599
|
WHERE facts_fts MATCH ? AND (f.project = ? OR f.project = '*') AND f.superseded_by IS NULL
|
|
600
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
495
601
|
ORDER BY rank LIMIT ?
|
|
496
602
|
`;
|
|
497
603
|
params = [sanitized, project, limit];
|
|
@@ -500,6 +606,7 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
500
606
|
SELECT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at, rank
|
|
501
607
|
FROM facts_fts fts JOIN facts f ON f.rowid = fts.rowid
|
|
502
608
|
WHERE facts_fts MATCH ? AND f.superseded_by IS NULL
|
|
609
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
503
610
|
ORDER BY rank LIMIT ?
|
|
504
611
|
`;
|
|
505
612
|
params = [sanitized, limit];
|
|
@@ -518,20 +625,24 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
518
625
|
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
519
626
|
AND ((scope = ? OR scope = '*') OR (scope IS NULL AND (project = ? OR project = '*')))
|
|
520
627
|
AND superseded_by IS NULL
|
|
628
|
+
AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
521
629
|
ORDER BY created_at DESC LIMIT ?`
|
|
522
630
|
: scope
|
|
523
631
|
? `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
524
632
|
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
525
633
|
AND (scope = ? OR scope = '*') AND superseded_by IS NULL
|
|
634
|
+
AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
526
635
|
ORDER BY created_at DESC LIMIT ?`
|
|
527
636
|
: project
|
|
528
637
|
? `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
529
638
|
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
530
639
|
AND (project = ? OR project = '*') AND superseded_by IS NULL
|
|
640
|
+
AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
531
641
|
ORDER BY created_at DESC LIMIT ?`
|
|
532
642
|
: `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
533
643
|
FROM facts WHERE (entity LIKE ? OR value LIKE ? OR tags LIKE ?)
|
|
534
644
|
AND superseded_by IS NULL
|
|
645
|
+
AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))
|
|
535
646
|
ORDER BY created_at DESC LIMIT ?`;
|
|
536
647
|
const likeResults = scope && project
|
|
537
648
|
? db.prepare(likeSql).all(like, like, like, scope, project, limit)
|
|
@@ -697,8 +808,35 @@ function stats() {
|
|
|
697
808
|
/**
|
|
698
809
|
* Close the database connection (for clean shutdown).
|
|
699
810
|
*/
|
|
700
|
-
|
|
811
|
+
/**
|
|
812
|
+
* Acquire a reference. Call once per logical "session" (e.g. per task run).
|
|
813
|
+
* Ensures DB is open and increments the ref count.
|
|
814
|
+
* Must be paired with a matching release() call.
|
|
815
|
+
*/
|
|
816
|
+
function acquire() {
|
|
817
|
+
_refCount++;
|
|
818
|
+
getDb(); // ensure DB is initialised
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Release a reference. When the last caller releases, the DB is closed.
|
|
823
|
+
* Safe to call even if acquire() was never called (no-op when _refCount <= 0).
|
|
824
|
+
*/
|
|
825
|
+
function release() {
|
|
826
|
+
if (_refCount > 0) _refCount--;
|
|
827
|
+
if (_refCount === 0 && _db) { _db.close(); _db = null; }
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Backwards-compatible alias. Equivalent to release().
|
|
832
|
+
* External callers that previously called close() continue to work correctly.
|
|
833
|
+
*/
|
|
834
|
+
function close() { release(); }
|
|
835
|
+
|
|
836
|
+
/** Force-close regardless of ref count. Only call on process exit. */
|
|
837
|
+
function forceClose() {
|
|
838
|
+
_refCount = 0;
|
|
701
839
|
if (_db) { _db.close(); _db = null; }
|
|
702
840
|
}
|
|
703
841
|
|
|
704
|
-
module.exports = { saveSession, saveFacts, searchFacts, searchFactsAsync, searchSessions, recentSessions, getSession, stats, close, DB_PATH };
|
|
842
|
+
module.exports = { saveSession, saveFacts, searchFacts, searchFactsAsync, searchSessions, recentSessions, getSession, stats, acquire, release, close, forceClose, DB_PATH };
|
package/scripts/schema.js
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
* T1 — Identity (LOCKED, never auto-modify)
|
|
12
12
|
* T2 — Core Values (LOCKED, deep personality)
|
|
13
13
|
* T3 — Preferences (auto-writable, needs confidence)
|
|
14
|
-
* T4 — Context (free overwrite, current state)
|
|
15
14
|
* T5 — Evolution (system-managed, strict limits)
|
|
15
|
+
*
|
|
16
|
+
* NOTE: T4 (Context/Status) was intentionally removed. Work state (focus,
|
|
17
|
+
* active_projects, blockers) belongs in ~/.metame/memory/NOW.md (task
|
|
18
|
+
* whiteboard), not in the cognitive profile. This prevents role pollution.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
const SCHEMA = {
|
|
@@ -49,14 +52,15 @@ const SCHEMA = {
|
|
|
49
52
|
'cognition.metacognition.receptive_to_challenge': { tier: 'T3', type: 'enum', values: ['yes', 'sometimes', 'no'] },
|
|
50
53
|
'cognition.metacognition.error_response': { tier: 'T3', type: 'enum', values: ['quick_pivot', 'root_cause_first', 'seek_help', 'retry_same'] },
|
|
51
54
|
|
|
52
|
-
// ===
|
|
53
|
-
'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
// === T3c: User Competence Map (ZPD scaffolding) ===
|
|
56
|
+
'user_competence_map': {
|
|
57
|
+
tier: 'T3',
|
|
58
|
+
type: 'map', // dynamic key-value, keys are domain names
|
|
59
|
+
valueType: 'enum',
|
|
60
|
+
values: ['beginner', 'intermediate', 'expert'],
|
|
61
|
+
maxKeys: 20,
|
|
62
|
+
description: 'Per-domain skill level for ZPD-based explanation depth'
|
|
63
|
+
},
|
|
60
64
|
|
|
61
65
|
// === T5: Evolution (system-managed) ===
|
|
62
66
|
'evolution.last_distill': { tier: 'T5', type: 'string' },
|
|
@@ -144,6 +148,23 @@ function validate(key, value) {
|
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
if (def.type === 'map') {
|
|
152
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
153
|
+
return { valid: false, reason: `${key} must be an object (map)` };
|
|
154
|
+
}
|
|
155
|
+
if (def.maxKeys && Object.keys(value).length > def.maxKeys) {
|
|
156
|
+
return { valid: false, reason: `${key} exceeds maxKeys (${def.maxKeys})` };
|
|
157
|
+
}
|
|
158
|
+
if (def.values) {
|
|
159
|
+
for (const [k, v] of Object.entries(value)) {
|
|
160
|
+
if (!def.values.includes(v)) {
|
|
161
|
+
return { valid: false, reason: `${key}.${k} must be one of: ${def.values.join(', ')}` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { valid: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
147
168
|
return { valid: true };
|
|
148
169
|
}
|
|
149
170
|
|