@yottameta/yotta-memory 0.17.3 → 0.18.0
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/CHANGELOG.md +55 -0
- package/README.md +3 -1
- package/README.zh-CN.md +9 -6
- package/SKILL.md +10 -5
- package/USER_GUIDE.md +14 -5
- package/bin/yotta-memory.js +1017 -72
- package/package.json +1 -1
- package/references/faq.md +9 -0
- package/references/protocol.md +28 -3
- package/skill-manifest.json +1 -1
package/bin/yotta-memory.js
CHANGED
|
@@ -25,9 +25,9 @@ const net = require('net');
|
|
|
25
25
|
const child_process = require('child_process');
|
|
26
26
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
27
27
|
|
|
28
|
-
const VERSION = '0.
|
|
29
|
-
const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path']);
|
|
30
|
-
const CLI_FLAG_OPTS = new Set(['--project', '--all', '--unsafe', '--no-auth', '--stdio', '--onstart', '--from-current', '--restart', '--force', '--attach', '--allow-same-volume', '--verify', '--no-hint', '--encrypt', '--no-encrypt', '--password-stdin', '--json', '--manual', '--skip-schedule', '--useful', '--useless', '--undo', '--dry-run', '--apply', '--purge', '--dedup', '--batches', '--explain', '--semantic', '--runtime', '--ablate', '--timing', '--baseline', '--probe', '--quarantine', '--restore', '--yes', '--keep-memories', '--keep-identity']);
|
|
28
|
+
const VERSION = '0.18.0';
|
|
29
|
+
const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path', '--from']);
|
|
30
|
+
const CLI_FLAG_OPTS = new Set(['--project', '--all', '--unsafe', '--no-auth', '--stdio', '--onstart', '--from-current', '--restart', '--force', '--attach', '--allow-same-volume', '--verify', '--no-hint', '--encrypt', '--no-encrypt', '--password-stdin', '--json', '--manual', '--skip-schedule', '--useful', '--useless', '--undo', '--dry-run', '--apply', '--purge', '--dedup', '--batches', '--propose', '--audit', '--capacity', '--explain', '--semantic', '--runtime', '--ablate', '--timing', '--baseline', '--probe', '--quarantine', '--restore', '--no-usage', '--yes', '--keep-memories', '--keep-identity']);
|
|
31
31
|
|
|
32
32
|
function helpOption(flag, arg, what, when, caution) {
|
|
33
33
|
return { flag: flag, arg: arg || '', what: what, when: when || '', caution: caution || '' };
|
|
@@ -65,15 +65,22 @@ const HELP_MODEL = [
|
|
|
65
65
|
helpOption('--embedding', '<命令>', '临时指定本地 embedding 插件命令', '这次检索想使用自定义本地向量命令时', '只在本地执行,不会把记忆上传远端'),
|
|
66
66
|
helpOption('--embedding-timeout', '<毫秒>', '设置 embedding 插件超时', '插件较慢或需要快速失败时', ''),
|
|
67
67
|
helpOption('--year', '<yyyy>', '只检索指定年份的记忆', '库很大、只想在某一年的记忆里找时', '可重复传多次;不传则检索全部年份'),
|
|
68
|
+
helpOption('--no-usage', '', '本次不记录命中打点', '只想临时关闭使用统计时', '只影响这一次调用,不改配置'),
|
|
68
69
|
] },
|
|
69
70
|
{ name: 'forget', usage: 'forget <记忆 id> [选项]', what: '删除一条记忆', when: '确认某条记忆不应继续保留时', options: [
|
|
70
71
|
helpOption('--reason', '<原因>', '记录删除原因', '需要留下为什么删除的审计线索时', ''),
|
|
71
72
|
helpOption('--unsafe', '', '跳过越界删除保护', '只有获得明确授权处理隔离库时', '会放宽删除边界,误删风险很高'),
|
|
72
73
|
] },
|
|
74
|
+
{ name: 'rename', usage: 'rename <记忆 id> <新文件名> [选项]', what: '给单条记忆改名(内容与身份不变)', when: '平铺 / 分层出现同序号冲突、或想整理文件名时', options: [
|
|
75
|
+
helpOption('--dry-run', '', '只预览,不改库', '想先确认改到哪个文件名时', '预演只读,不改任何文件'),
|
|
76
|
+
helpOption('--reason', '<原因>', '记录改名原因', '需要留下为什么改名的审计线索时', ''),
|
|
77
|
+
helpOption('--unsafe', '', '跳过越界写入保护', '只有获得明确授权处理隔离库时', '会放宽写入边界,谨慎使用'),
|
|
78
|
+
] },
|
|
73
79
|
{ name: 'archive', usage: 'archive [选项]', what: '把低价值或过期记忆归档', when: '库变大,想让旧记忆退出主检索但保留可回溯时', options: [
|
|
74
80
|
helpOption('--days', '<天数>', '按未使用天数判断归档', '想调整「多久没用才归档」时', ''),
|
|
75
81
|
helpOption('--threshold', '<数值>', '按效用分阈值判断归档', '想调整「用得少不少」时', ''),
|
|
76
|
-
helpOption('--dry-run', '', '只预览归档结果', '先看会动哪些记忆,再决定是否执行时', ''),
|
|
82
|
+
helpOption('--dry-run', '', '只预览归档结果', '先看会动哪些记忆,再决定是否执行时', '预演只读,不改任何文件'),
|
|
83
|
+
helpOption('--force', '', '关闭冷却期与标签常青豁免', '确认新条目或 evergreen / pinned 条目也要归档时', 'immutable 与 BOUND 仍然豁免;先确认清单'),
|
|
77
84
|
] },
|
|
78
85
|
{ name: 'backup', usage: 'backup <子命令> [选项]', what: '备份、恢复和演练记忆库', when: '配置自动备份、创建备份、恢复误删或验证备份可用时', subcommands: [
|
|
79
86
|
helpSub('volumes', 'volumes [--json]', '列出可用备份卷', '想知道备份会写到哪里时', [helpOption('--json', '', '输出 JSON', '脚本处理备份卷列表时', '')]),
|
|
@@ -110,6 +117,7 @@ const HELP_MODEL = [
|
|
|
110
117
|
helpOption('--json', '', '输出 JSON', '脚本或自动化读取体检结果时', ''),
|
|
111
118
|
] },
|
|
112
119
|
{ name: 'maintain', usage: 'maintain [选项]', what: '记忆自组织:归档、遗忘候选、去重和合并', when: '定期清理低价值记忆、合并重复条目时', options: [
|
|
120
|
+
helpOption('--capacity', '', '查看容量水位与淘汰 / 晋升建议', '库变大,想看哪些条目值得清理或提升时', '只读报告,不会自动归档或改权重'),
|
|
113
121
|
helpOption('--dedup', '', '只做重复检查并给置信度', '想先看哪些条目重复时', ''),
|
|
114
122
|
helpOption('--apply', '', '真正执行,而不是只预览', '确认预览结果后要落盘时', '会改动记忆库并创建事务快照;先跑 dry-run'),
|
|
115
123
|
helpOption('--purge', '', '对遗忘候选执行真删', '确认候选确实不再需要时', '真删不可逆;通常先用归档而不是 purge'),
|
|
@@ -121,8 +129,10 @@ const HELP_MODEL = [
|
|
|
121
129
|
helpOption('--merge', '<id列表>', '显式合并指定条目', '已经确认这些条目可以合并时', ''),
|
|
122
130
|
helpOption('--json', '', '输出 JSON', '自动化处理维护结果时', ''),
|
|
123
131
|
] },
|
|
124
|
-
{ name: 'consolidate', usage: 'consolidate [--apply | --undo <batch> | --batches] [选项]', what: '把旧记忆压缩成周期摘要', when: '库很大,想把超龄、闲置、低效用条目压成摘要时', options: [
|
|
125
|
-
helpOption('--
|
|
132
|
+
{ name: 'consolidate', usage: 'consolidate [--propose | --apply | --undo <batch> | --batches] [选项]', what: '把旧记忆压缩成周期摘要', when: '库很大,想把超龄、闲置、低效用条目压成摘要时', options: [
|
|
133
|
+
helpOption('--propose', '', '只输出结构化候选报告(默认行为)', '想先审阅会压缩哪些内容、保留与回滚口径时', '不写盘;等价于直接运行 consolidate'),
|
|
134
|
+
helpOption('--apply', '', '真正执行压缩', '确认 propose 报告后要落盘时', '会改动记忆库并创建事务快照;先预览'),
|
|
135
|
+
helpOption('--yes', '', '非交互确认执行', '脚本或自动化环境里已经审阅过 propose 报告时', '跳过交互确认;破坏性操作,务必先看报告'),
|
|
126
136
|
helpOption('--undo', '<batch>', '回滚一个摘要批次', '压缩后发现结果不合适时', ''),
|
|
127
137
|
helpOption('--batches', '', '列出历史批次', '想找回滚批次号时', ''),
|
|
128
138
|
helpOption('--period', '<天数>', '设置摘要周期', '想改变摘要覆盖的时间跨度时', ''),
|
|
@@ -143,7 +153,10 @@ const HELP_MODEL = [
|
|
|
143
153
|
helpOption('--useless', '', '记为没用', '这条记忆造成误导或噪音时', ''),
|
|
144
154
|
helpOption('--undo', '', '撤销上一次反馈', '误点反馈时', ''),
|
|
145
155
|
] },
|
|
146
|
-
{ name: 'explain', usage: 'explain <记忆 id> [--json]', what: '解释一条记忆的效用分和归档判定', when: '想理解为什么它被排序、归档或遗忘时', options: [
|
|
156
|
+
{ name: 'explain', usage: 'explain <记忆 id> [--json] [--no-usage]', what: '解释一条记忆的效用分和归档判定', when: '想理解为什么它被排序、归档或遗忘时', options: [
|
|
157
|
+
helpOption('--json', '', '输出 JSON', '脚本读取解释结果时', ''),
|
|
158
|
+
helpOption('--no-usage', '', '本次不记录命中打点', '只想临时关闭使用统计时', '只影响这一次调用,不改配置'),
|
|
159
|
+
] },
|
|
147
160
|
{ name: 'bench', usage: 'bench [--evalset <文件>] [--k <条数>] [--seed <数值>] [--bootstrap <次数>] [--ablate] [--gate <指标>=<数值>] [--timing] [--json] [--out <文件>]', what: '可复算的检索基准评测', when: '改检索 / 索引逻辑前后要证明「没变差」,或把质量门禁接进 CI 时', options: [
|
|
148
161
|
helpOption('--evalset', '<文件>', '指定评测集 JSON 文件', '想固定一组查询来复算指标时', '不指定时按库内条目做确定性抽样,生成基线评测集'),
|
|
149
162
|
helpOption('--k', '<条数>', '设置结果窗口大小', '想按 Recall@k / nDCG@k 的 k 口径统计时', '默认 5'),
|
|
@@ -190,7 +203,9 @@ const HELP_MODEL = [
|
|
|
190
203
|
helpOption('--owner', '<id>', '指定画像归属者', '查某个 owner 的私密画像时', '仍然受私密区权限约束'),
|
|
191
204
|
helpOption('--json', '', '输出 JSON', '自动化读取画像时', ''),
|
|
192
205
|
] },
|
|
193
|
-
{ name: 'context', usage: 'context [选项]', what: '生成开工上下文包', when: '每个会话开工时恢复身份、边界、近期记忆和承诺', options: [
|
|
206
|
+
{ name: 'context', usage: 'context [--audit [--from <文件|->]] [选项]', what: '生成开工上下文包', when: '每个会话开工时恢复身份、边界、近期记忆和承诺', options: [
|
|
207
|
+
helpOption('--audit', '', '审计被压缩掉的内容是否已经落盘', '宿主压缩上下文后,想确认有没有决策只存在于对话里时', '只读:只打印 remember 建议,不会自动补写'),
|
|
208
|
+
helpOption('--from', '<文件|->', '指定待审计内容(- 表示标准输入)', '把宿主压缩摘要或丢弃段落喂给审计时', '不传时审计当前上下文包的丢弃清单'),
|
|
194
209
|
helpOption('--limit', '<条数>', '限制近期记忆条数', '上下文太长、想缩短时', ''),
|
|
195
210
|
helpOption('--owner', '<id>', '指定 owner 范围', '需要看某个 owner 的上下文时', '仍然受私密区权限约束'),
|
|
196
211
|
helpOption('--budget', '<字符数>', '设置动态记忆字符预算', '需要控制上下文包大小时', '0 表示不限制'),
|
|
@@ -199,6 +214,7 @@ const HELP_MODEL = [
|
|
|
199
214
|
helpOption('--embedding', '<命令>', '临时指定本地 embedding 插件命令', '上下文检索想用本地向量命令时', '只在本地执行'),
|
|
200
215
|
helpOption('--embedding-timeout', '<毫秒>', '设置 embedding 插件超时', '插件较慢或需要快速失败时', ''),
|
|
201
216
|
helpOption('--year', '<yyyy>', '只装载指定年份的记忆', '只想让上下文包覆盖某一年份时', '可重复传多次;不传则装载全部年份'),
|
|
217
|
+
helpOption('--no-usage', '', '本次不记录命中打点', '只想临时关闭使用统计时', '只影响这一次调用,不改配置'),
|
|
202
218
|
] },
|
|
203
219
|
{ name: 'token', usage: 'token <子命令> [选项]', what: '管理远端 MCP 访问 token', when: '给远端 MCP 客户端发放或吊销访问凭据时', subcommands: [
|
|
204
220
|
helpSub('new', 'new --agent <id> [--scope <范围>] [--json]', '生成一个访问 token', '远端客户端第一次接入时', [
|
|
@@ -299,7 +315,7 @@ const PRIVATE_LEAF = ['prefs', 'bounds', 'commits'];
|
|
|
299
315
|
const ARCHIVE_DIR = '.archive';
|
|
300
316
|
const INDEX_FILE = 'index.json';
|
|
301
317
|
const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
|
|
302
|
-
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed', 'feedback_net'];
|
|
318
|
+
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed', 'feedback_net', 'hit_days', 'hit_queries'];
|
|
303
319
|
const CONFIG_FILE = 'config.json';
|
|
304
320
|
const SERVER_SUBDIR = '.server';
|
|
305
321
|
const TOKENS_FILE = 'tokens.json';
|
|
@@ -1325,6 +1341,129 @@ function semanticMatch(entry, query, wantDetail) {
|
|
|
1325
1341
|
function round3(n) { return Math.round(n * 1000) / 1000; }
|
|
1326
1342
|
// ---- v0.8.0 自我进化:统一效用分(盖棺分)----
|
|
1327
1343
|
function feedbackNetOf(meta) { return parseInt(meta.feedback_net || '0', 10) || 0; }
|
|
1344
|
+
// ---- v0.18.0 A3:命中打点(本地元数据;查询只存 8 位指纹,不落原文)----
|
|
1345
|
+
// hit_days 形如 s2026-09-26:2|g2026-09-25:1|u2026-09-24:1(s=search / g=get / u=use)。
|
|
1346
|
+
// hit_queries 形如 q3f9a1c7:2|q77bd021e:1(sha256(query) 前 8 位十六进制 + 次数)。
|
|
1347
|
+
const USAGE_SIGNAL_LABELS = { search: 's', get: 'g', use: 'u' };
|
|
1348
|
+
const USAGE_SIGNAL_ORDER = { s: 0, g: 1, u: 2 };
|
|
1349
|
+
const USAGE_CONFIG_DEFAULTS = { usage_retention_days: 90, usage_query_slots: 12 };
|
|
1350
|
+
function usageNumber(cfg, key) {
|
|
1351
|
+
const raw = cfg ? cfg[key] : undefined;
|
|
1352
|
+
if (raw === undefined || raw === null || raw === '') return USAGE_CONFIG_DEFAULTS[key];
|
|
1353
|
+
const n = parseFloat(raw);
|
|
1354
|
+
return (n >= 0) ? n : USAGE_CONFIG_DEFAULTS[key];
|
|
1355
|
+
}
|
|
1356
|
+
function usageSettings(opts) {
|
|
1357
|
+
const cfg = loadConfig();
|
|
1358
|
+
return {
|
|
1359
|
+
enabled: !(opts && opts.noUsage) && cfg.usage_enabled !== false,
|
|
1360
|
+
retentionDays: usageNumber(cfg, 'usage_retention_days'),
|
|
1361
|
+
querySlots: usageNumber(cfg, 'usage_query_slots'),
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
function usageFingerprint(query) {
|
|
1365
|
+
return crypto.createHash('sha256').update(String(query), 'utf8').digest('hex').slice(0, 8);
|
|
1366
|
+
}
|
|
1367
|
+
function parseHitDays(value) {
|
|
1368
|
+
const map = {};
|
|
1369
|
+
const re = /([sgu])(\d{4}-\d{2}-\d{2}):(\d+)/g;
|
|
1370
|
+
let m;
|
|
1371
|
+
while ((m = re.exec(String(value || '')))) {
|
|
1372
|
+
const key = m[1] + m[2];
|
|
1373
|
+
map[key] = (map[key] || 0) + (parseInt(m[3], 10) || 0);
|
|
1374
|
+
}
|
|
1375
|
+
return map;
|
|
1376
|
+
}
|
|
1377
|
+
function serializeHitDays(map) {
|
|
1378
|
+
const keys = Object.keys(map).filter(function (k) { return /^[sgu]\d{4}-\d{2}-\d{2}$/.test(k) && map[k] > 0; });
|
|
1379
|
+
keys.sort(function (a, b) {
|
|
1380
|
+
const da = a.slice(1), db = b.slice(1);
|
|
1381
|
+
if (da !== db) return db.localeCompare(da);
|
|
1382
|
+
return (USAGE_SIGNAL_ORDER[a[0]] || 0) - (USAGE_SIGNAL_ORDER[b[0]] || 0);
|
|
1383
|
+
});
|
|
1384
|
+
return keys.map(function (k) { return k + ':' + map[k]; }).join('|');
|
|
1385
|
+
}
|
|
1386
|
+
function parseHitQueries(value) {
|
|
1387
|
+
const map = {};
|
|
1388
|
+
const re = /q([0-9a-f]{8}):(\d+)/g;
|
|
1389
|
+
let m;
|
|
1390
|
+
while ((m = re.exec(String(value || '')))) {
|
|
1391
|
+
map[m[1]] = (map[m[1]] || 0) + (parseInt(m[2], 10) || 0);
|
|
1392
|
+
}
|
|
1393
|
+
return map;
|
|
1394
|
+
}
|
|
1395
|
+
function serializeHitQueries(map, slots) {
|
|
1396
|
+
const fps = Object.keys(map).filter(function (fp) { return /^[0-9a-f]{8}$/.test(fp) && map[fp] > 0; });
|
|
1397
|
+
fps.sort(function (a, b) {
|
|
1398
|
+
if (map[b] !== map[a]) return map[b] - map[a];
|
|
1399
|
+
return a.localeCompare(b);
|
|
1400
|
+
});
|
|
1401
|
+
const limit = (slots >= 0) ? slots : USAGE_CONFIG_DEFAULTS.usage_query_slots;
|
|
1402
|
+
return fps.slice(0, limit).map(function (fp) { return 'q' + fp + ':' + map[fp]; }).join('|');
|
|
1403
|
+
}
|
|
1404
|
+
// 纯函数:算好本次命中要写入的 frontmatter 字段(不写盘,便于 recall/feedback 复用同一口径)。
|
|
1405
|
+
function usagePatchForMeta(meta, signal, query, settings, dateStr) {
|
|
1406
|
+
const label = USAGE_SIGNAL_LABELS[signal];
|
|
1407
|
+
if (!label) return {};
|
|
1408
|
+
const patch = {};
|
|
1409
|
+
const days = parseHitDays(meta.hit_days);
|
|
1410
|
+
const key = label + dateStr;
|
|
1411
|
+
days[key] = (days[key] || 0) + 1;
|
|
1412
|
+
const kept = {};
|
|
1413
|
+
for (const k of Object.keys(days)) {
|
|
1414
|
+
const age = daysBetween(k.slice(1), dateStr);
|
|
1415
|
+
if (age < 0 || age > settings.retentionDays) continue;
|
|
1416
|
+
kept[k] = days[k];
|
|
1417
|
+
}
|
|
1418
|
+
const daysStr = serializeHitDays(kept);
|
|
1419
|
+
if (daysStr) patch.hit_days = daysStr;
|
|
1420
|
+
if (signal === 'search' && query) {
|
|
1421
|
+
const queries = parseHitQueries(meta.hit_queries);
|
|
1422
|
+
const fp = usageFingerprint(query);
|
|
1423
|
+
queries[fp] = (queries[fp] || 0) + 1;
|
|
1424
|
+
patch.hit_queries = serializeHitQueries(queries, settings.querySlots);
|
|
1425
|
+
}
|
|
1426
|
+
return patch;
|
|
1427
|
+
}
|
|
1428
|
+
// 独立写点(explain / context use 信号):写失败只计数,不影响读取结果。
|
|
1429
|
+
function applyUsageHits(root, relFiles, signal, query, opts) {
|
|
1430
|
+
opts = opts || {};
|
|
1431
|
+
const out = { enabled: false, written: 0, skipped: 0, failed: 0 };
|
|
1432
|
+
const settings = usageSettings(opts);
|
|
1433
|
+
out.enabled = settings.enabled;
|
|
1434
|
+
if (!settings.enabled) return out;
|
|
1435
|
+
const selfAgent = (opts.selfAgent !== undefined) ? opts.selfAgent : resolveIdentity(opts).id;
|
|
1436
|
+
const dateStr = today();
|
|
1437
|
+
for (const rel of relFiles) {
|
|
1438
|
+
const fp = path.join(root, rel);
|
|
1439
|
+
if (!fs.existsSync(fp)) continue;
|
|
1440
|
+
const owner = ownerFromPrivatePath(root, fp);
|
|
1441
|
+
if (owner && owner !== selfAgent) { out.skipped++; continue; }
|
|
1442
|
+
try {
|
|
1443
|
+
const parsed = parseFrontmatter(readMemoryText(root, fp, owner));
|
|
1444
|
+
if (parsed.meta.immutable === 'true') { out.skipped++; continue; }
|
|
1445
|
+
const patch = usagePatchForMeta(parsed.meta, signal, query, settings, dateStr);
|
|
1446
|
+
if (!Object.keys(patch).length) continue;
|
|
1447
|
+
const meta = Object.assign({}, parsed.meta, patch);
|
|
1448
|
+
writeMemoryText(root, fp, frontmatterToText(meta, parsed.body), owner);
|
|
1449
|
+
upsertIndexEntry(root, readEntry(fp, root));
|
|
1450
|
+
out.written++;
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
out.failed++;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return out;
|
|
1456
|
+
}
|
|
1457
|
+
// 报告侧派生:窗口内命中次数(不落盘,避免派生值与原始值漂移)。
|
|
1458
|
+
function usageTotals(entry, days) {
|
|
1459
|
+
const map = parseHitDays(entry && entry.hit_days);
|
|
1460
|
+
let total = 0;
|
|
1461
|
+
for (const k of Object.keys(map)) {
|
|
1462
|
+
const age = daysBetween(k.slice(1), today());
|
|
1463
|
+
if (age >= 0 && age <= days) total += map[k];
|
|
1464
|
+
}
|
|
1465
|
+
return total;
|
|
1466
|
+
}
|
|
1328
1467
|
function utilityScore(meta) {
|
|
1329
1468
|
const conf = Math.min(Math.max(parseFloat(meta.confidence || 1.0) || 1.0, 0), 1);
|
|
1330
1469
|
const acc = parseInt(meta.access_count || '0', 10) || 0;
|
|
@@ -2294,6 +2433,8 @@ function readEntry(fp, root) {
|
|
|
2294
2433
|
source: meta.source || '',
|
|
2295
2434
|
weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
|
|
2296
2435
|
feedback_net: feedbackNetOf(meta),
|
|
2436
|
+
hit_days: meta.hit_days || '',
|
|
2437
|
+
hit_queries: meta.hit_queries || '',
|
|
2297
2438
|
tokens: buildTokens(subject, statement, tags),
|
|
2298
2439
|
meta: meta,
|
|
2299
2440
|
};
|
|
@@ -2307,23 +2448,72 @@ function rewriteFrontmatter(fp, patch, root, owner) {
|
|
|
2307
2448
|
for (const k of Object.keys(patch)) meta[k] = patch[k];
|
|
2308
2449
|
writeMemoryText(root || memoryRootForFile(fp), fp, frontmatterToText(meta, parsed.body), owner || ownerFromPrivatePath(root || memoryRootForFile(fp), fp));
|
|
2309
2450
|
}
|
|
2310
|
-
function bumpReadMeta(root, relFiles) {
|
|
2451
|
+
function bumpReadMeta(root, relFiles, options) {
|
|
2452
|
+
options = options || {};
|
|
2311
2453
|
const now = today();
|
|
2454
|
+
const settings = usageSettings(options);
|
|
2455
|
+
const selfAgent = (options.selfAgent !== undefined) ? options.selfAgent : resolveIdentity(options).id;
|
|
2456
|
+
const usageByFile = {};
|
|
2312
2457
|
for (const rel of relFiles) {
|
|
2313
2458
|
const fp = path.join(root, rel);
|
|
2314
2459
|
if (!fs.existsSync(fp)) continue;
|
|
2315
2460
|
const owner = ownerFromPrivatePath(root, fp);
|
|
2461
|
+
// v0.18.0:跨 owner 私密条目一律不写(读得到也不代表能改别人的文件)。
|
|
2462
|
+
if (owner && owner !== selfAgent) continue;
|
|
2316
2463
|
let parsed;
|
|
2317
2464
|
try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (e) { continue; }
|
|
2318
2465
|
const meta = parsed.meta;
|
|
2319
2466
|
if (meta.immutable === 'true') continue;
|
|
2320
2467
|
const acc = (parseInt(meta.access_count || '0', 10) || 0) + 1;
|
|
2321
|
-
|
|
2468
|
+
const patch = { access_count: acc, last_accessed: now };
|
|
2469
|
+
if (settings.enabled) {
|
|
2470
|
+
const usagePatch = usagePatchForMeta(meta, 'search', options.query || '', settings, now);
|
|
2471
|
+
Object.assign(patch, usagePatch);
|
|
2472
|
+
usageByFile[rel] = usagePatch;
|
|
2473
|
+
}
|
|
2474
|
+
try {
|
|
2475
|
+
rewriteFrontmatter(fp, patch, root, owner);
|
|
2476
|
+
} catch (e) {
|
|
2477
|
+
// 打点写失败不影响读取结果本身;索引也保持原样。
|
|
2478
|
+
delete usageByFile[rel];
|
|
2479
|
+
}
|
|
2322
2480
|
}
|
|
2481
|
+
return { usageByFile: usageByFile };
|
|
2323
2482
|
}
|
|
2324
2483
|
function touchIndex(root, relFiles, options) {
|
|
2484
|
+
options = options || {};
|
|
2325
2485
|
const set = new Set(relFiles);
|
|
2326
2486
|
const now = today();
|
|
2487
|
+
const usageByFile = options.usageByFile || {};
|
|
2488
|
+
const bumpEntry = function (e) {
|
|
2489
|
+
e.access_count = (parseInt(e.access_count, 10) || 0) + 1;
|
|
2490
|
+
e.last_accessed = now;
|
|
2491
|
+
const usage = usageByFile[e.file];
|
|
2492
|
+
if (usage) {
|
|
2493
|
+
if (usage.hit_days !== undefined) e.hit_days = usage.hit_days;
|
|
2494
|
+
if (usage.hit_queries !== undefined) e.hit_queries = usage.hit_queries;
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
// v0.18.0:加密库的私密条目命中必须当场同步 owner index(旧版会滞后到 reindex)。
|
|
2498
|
+
if (isEncrypted(root)) {
|
|
2499
|
+
const byOwner = {};
|
|
2500
|
+
for (const rel of set) {
|
|
2501
|
+
if (String(rel).indexOf(PRIVATE_DIR + '/') !== 0) continue;
|
|
2502
|
+
const owner = String(rel).split('/')[1] || '';
|
|
2503
|
+
(byOwner[owner] = byOwner[owner] || []).push(rel);
|
|
2504
|
+
}
|
|
2505
|
+
for (const owner of Object.keys(byOwner)) {
|
|
2506
|
+
const key = getOwnerKeyFor(root, owner);
|
|
2507
|
+
if (!key) continue;
|
|
2508
|
+
let ownerIdx;
|
|
2509
|
+
try { ownerIdx = loadOwnerIndex(root, owner, key); } catch (e) { continue; }
|
|
2510
|
+
let dirty = false;
|
|
2511
|
+
for (const e of ownerIdx) {
|
|
2512
|
+
if (set.has(e.file)) { bumpEntry(e); dirty = true; }
|
|
2513
|
+
}
|
|
2514
|
+
if (dirty) saveOwnerIndex(root, owner, key, ownerIdx);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2327
2517
|
// v0.17.0 B3:带 --year 的检索只碰命中年份的分片(命中条目必然来自这些分片),
|
|
2328
2518
|
// 不传年份时行为与旧版一致(遍历全部 shards)。
|
|
2329
2519
|
const filterYears = (options && Array.isArray(options.years)) ? options.years : [];
|
|
@@ -2339,7 +2529,7 @@ function touchIndex(root, relFiles, options) {
|
|
|
2339
2529
|
if (!sd || !Array.isArray(sd.entries)) continue;
|
|
2340
2530
|
let dirty = false;
|
|
2341
2531
|
for (const e of sd.entries) {
|
|
2342
|
-
if (set.has(e.file)) {
|
|
2532
|
+
if (set.has(e.file)) { bumpEntry(e); dirty = true; }
|
|
2343
2533
|
}
|
|
2344
2534
|
if (dirty) fs.writeFileSync(sp, JSON.stringify(sd, null, 2), 'utf8');
|
|
2345
2535
|
}
|
|
@@ -2348,7 +2538,7 @@ function touchIndex(root, relFiles, options) {
|
|
|
2348
2538
|
const idx = getIndex(root);
|
|
2349
2539
|
let dirty = false;
|
|
2350
2540
|
for (const e of idx) {
|
|
2351
|
-
if (set.has(e.file)) {
|
|
2541
|
+
if (set.has(e.file)) { bumpEntry(e); dirty = true; }
|
|
2352
2542
|
}
|
|
2353
2543
|
if (dirty) saveIndex(root, idx);
|
|
2354
2544
|
}
|
|
@@ -3486,6 +3676,10 @@ const SCALE_THRESHOLD_DEFAULTS = {
|
|
|
3486
3676
|
scale_warn_files_per_dir: 500,
|
|
3487
3677
|
scale_warn_index_bytes: 5 * 1024 * 1024,
|
|
3488
3678
|
scale_warn_cold_start_ms: 2000,
|
|
3679
|
+
scale_info_entries: 30000,
|
|
3680
|
+
scale_info_files_per_dir: 300,
|
|
3681
|
+
scale_info_index_bytes: 3 * 1024 * 1024,
|
|
3682
|
+
scale_info_cold_start_ms: 1200,
|
|
3489
3683
|
};
|
|
3490
3684
|
function scaleThreshold(cfg, key) {
|
|
3491
3685
|
const raw = cfg ? cfg[key] : undefined;
|
|
@@ -3520,6 +3714,10 @@ function measureIndexColdStart(root) {
|
|
|
3520
3714
|
const elapsed = Number(process.hrtime.bigint() - started) / 1e6;
|
|
3521
3715
|
return Math.round(elapsed * 1000) / 1000;
|
|
3522
3716
|
}
|
|
3717
|
+
function scaleMetric(key, value, unit, info, warn) {
|
|
3718
|
+
const level = value > warn ? 'warning' : (value > info ? 'info' : 'ok');
|
|
3719
|
+
return { key: key, value: value, unit: unit, info: info, warn: warn, level: level };
|
|
3720
|
+
}
|
|
3523
3721
|
function scaleReport(root, cfg) {
|
|
3524
3722
|
const thresholds = {};
|
|
3525
3723
|
for (const key of Object.keys(SCALE_THRESHOLD_DEFAULTS)) thresholds[key] = scaleThreshold(cfg, key);
|
|
@@ -3531,26 +3729,37 @@ function scaleReport(root, cfg) {
|
|
|
3531
3729
|
for (let i = 0; i < 3; i++) samples.push(measureIndexColdStart(root));
|
|
3532
3730
|
samples.sort(function (a, b) { return a - b; });
|
|
3533
3731
|
const coldStartMs = samples[Math.floor(samples.length / 2)];
|
|
3732
|
+
const metrics = [
|
|
3733
|
+
scaleMetric('entries', entries, '条', thresholds.scale_info_entries, thresholds.scale_warn_entries),
|
|
3734
|
+
scaleMetric('files_per_dir', filesPerDir, '个文件', thresholds.scale_info_files_per_dir, thresholds.scale_warn_files_per_dir),
|
|
3735
|
+
scaleMetric('index_bytes', indexBytes, '字节', thresholds.scale_info_index_bytes, thresholds.scale_warn_index_bytes),
|
|
3736
|
+
scaleMetric('cold_start_ms', coldStartMs, 'ms', thresholds.scale_info_cold_start_ms, thresholds.scale_warn_cold_start_ms),
|
|
3737
|
+
];
|
|
3534
3738
|
const warnings = [];
|
|
3535
|
-
if (
|
|
3739
|
+
if (metrics[0].level === 'warning') {
|
|
3536
3740
|
warnings.push('规模: 记忆条数 ' + entries + ' 超过阈值 ' + thresholds.scale_warn_entries + '(考虑按年归档旧条目,或分批导出冷数据)');
|
|
3537
3741
|
}
|
|
3538
|
-
if (
|
|
3742
|
+
if (metrics[1].level === 'warning') {
|
|
3539
3743
|
warnings.push('规模: 单个目录文件数 ' + filesPerDir + ' 超过阈值 ' + thresholds.scale_warn_files_per_dir + '(检查是否有目录未按年/月分层)');
|
|
3540
3744
|
}
|
|
3541
|
-
if (
|
|
3745
|
+
if (metrics[2].level === 'warning') {
|
|
3542
3746
|
warnings.push('规模: 索引总体积 ' + indexBytes + ' 字节超过阈值 ' + thresholds.scale_warn_index_bytes + '(可用 reindex 重建,或按年份分片)');
|
|
3543
3747
|
}
|
|
3544
|
-
if (
|
|
3748
|
+
if (metrics[3].level === 'warning') {
|
|
3545
3749
|
warnings.push('规模: 索引冷启动 ' + coldStartMs + 'ms 超过阈值 ' + thresholds.scale_warn_cold_start_ms + 'ms(可用 reindex 重建索引)');
|
|
3546
3750
|
}
|
|
3751
|
+
// v0.18.0 A6:分级只影响 level 与提示文案;超 warn 仍进 warnings(既有行为),info 不阻断。
|
|
3752
|
+
const level = metrics.some(function (m) { return m.level === 'warning'; })
|
|
3753
|
+
? 'warning'
|
|
3754
|
+
: (metrics.some(function (m) { return m.level === 'info'; }) ? 'info' : 'ok');
|
|
3547
3755
|
return {
|
|
3548
3756
|
entries: entries,
|
|
3549
3757
|
files_per_dir: filesPerDir,
|
|
3550
3758
|
index_bytes: indexBytes,
|
|
3551
3759
|
cold_start_ms: coldStartMs,
|
|
3552
3760
|
thresholds: thresholds,
|
|
3553
|
-
|
|
3761
|
+
metrics: metrics,
|
|
3762
|
+
level: level,
|
|
3554
3763
|
warnings: warnings,
|
|
3555
3764
|
};
|
|
3556
3765
|
}
|
|
@@ -3717,7 +3926,13 @@ function doctorCore(opts) {
|
|
|
3717
3926
|
'- 备份目录: ' + (configuredDir || (cfg.backup_setup_choice === 'manual' ? '手动模式' : '(未配置)')),
|
|
3718
3927
|
];
|
|
3719
3928
|
if (checks.scale) {
|
|
3720
|
-
|
|
3929
|
+
const scaleLevelLabel = checks.scale.level === 'warning' ? '告警' : (checks.scale.level === 'info' ? '提示' : '正常');
|
|
3930
|
+
const scaleTop = (checks.scale.metrics || []).filter(function (m) { return m.level !== 'ok'; })
|
|
3931
|
+
.sort(function (a, b) {
|
|
3932
|
+
const rank = { warning: 2, info: 1, ok: 0 };
|
|
3933
|
+
return rank[b.level] - rank[a.level];
|
|
3934
|
+
})[0];
|
|
3935
|
+
lines.push('- 规模: ' + checks.scale.entries + ' 条记忆 / 单目录最大 ' + checks.scale.files_per_dir + ' 个文件 / 索引 ' + checks.scale.index_bytes + ' 字节 / 冷启动 ' + checks.scale.cold_start_ms + 'ms(' + scaleLevelLabel + (scaleTop ? ',最高项 ' + scaleTop.key + ' ' + scaleTop.value + scaleTop.unit : '') + ')');
|
|
3721
3936
|
}
|
|
3722
3937
|
if (checks.layout && (checks.layout.duplicates || checks.layout.conflicts || checks.layout.unverified)) {
|
|
3723
3938
|
lines.push('- 布局: 同序号重复 ' + checks.layout.duplicates + ' 组 / 冲突 ' + checks.layout.conflicts + ' 组 / 待核 ' + checks.layout.unverified + ' 组');
|
|
@@ -4400,11 +4615,19 @@ function recallCore(query, opts) {
|
|
|
4400
4615
|
}
|
|
4401
4616
|
return { error: false, exitCode: 0, text: '无匹配记忆。' };
|
|
4402
4617
|
}
|
|
4403
|
-
const
|
|
4404
|
-
|
|
4618
|
+
const touchAll = shown.filter(function (h) { return !h.entry.immutable; }).map(function (h) { return h.entry.file; });
|
|
4619
|
+
let usageSkipped = 0;
|
|
4620
|
+
if (touchAll.length) {
|
|
4405
4621
|
for (const root of roots) {
|
|
4406
|
-
|
|
4407
|
-
|
|
4622
|
+
const touchRel = touchAll.filter(function (rel) {
|
|
4623
|
+
if (!fs.existsSync(path.join(root, rel))) return false;
|
|
4624
|
+
const owner = ownerFromPrivatePath(root, path.join(root, rel));
|
|
4625
|
+
if (owner && owner !== selfAgent) { usageSkipped++; return false; }
|
|
4626
|
+
return true;
|
|
4627
|
+
});
|
|
4628
|
+
if (!touchRel.length) continue;
|
|
4629
|
+
const bump = bumpReadMeta(root, touchRel, { selfAgent: selfAgent, query: q, noUsage: !!opts.noUsage });
|
|
4630
|
+
touchIndex(root, touchRel, { years: years, usageByFile: bump.usageByFile });
|
|
4408
4631
|
}
|
|
4409
4632
|
}
|
|
4410
4633
|
const lines = ['共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):'];
|
|
@@ -4421,6 +4644,9 @@ function recallCore(query, opts) {
|
|
|
4421
4644
|
if (deniedCount > 0 && explicitCross) {
|
|
4422
4645
|
lines.push('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
|
|
4423
4646
|
}
|
|
4647
|
+
if (usageSkipped > 0) {
|
|
4648
|
+
lines.push('\n[提示] 跨 owner 私密条目未记录命中打点(' + usageSkipped + ' 条):只有条目所属 owner 才能更新自己的打点。');
|
|
4649
|
+
}
|
|
4424
4650
|
return {
|
|
4425
4651
|
error: false,
|
|
4426
4652
|
exitCode: 0,
|
|
@@ -4456,6 +4682,77 @@ function appendTrashAudit(root, record) {
|
|
|
4456
4682
|
fs.mkdirSync(dir, { recursive: true });
|
|
4457
4683
|
fs.appendFileSync(path.join(dir, 'audit-' + today() + '.jsonl'), JSON.stringify(record) + '\n', 'utf8');
|
|
4458
4684
|
}
|
|
4685
|
+
|
|
4686
|
+
// v0.17.4:给单条记忆改名(消除「平铺 / 分层同序号」冲突的最小手段)。
|
|
4687
|
+
// 身份键 = 类型 + owner + 文件名:目标名必须避开同身份占用,跨平铺 / 分层一起查;
|
|
4688
|
+
// 走 destructiveGuardCore(doctor + 独立备份 + 事务快照),改名后重建索引并写审计。
|
|
4689
|
+
function renameCore(fileRef, newName, opts) {
|
|
4690
|
+
opts = opts || {};
|
|
4691
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
4692
|
+
const target = resolveMemoryTarget(fileRef);
|
|
4693
|
+
if (!target) return { error: true, text: '未找到记忆文件: ' + fileRef };
|
|
4694
|
+
const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
|
|
4695
|
+
if (deny) return { error: true, text: deny };
|
|
4696
|
+
const name = String(newName || '').trim();
|
|
4697
|
+
if (!/^\d{4}-\d{2}-\d{2}-\d{4}\.md$/.test(name)) {
|
|
4698
|
+
return { error: true, text: '新文件名需形如 YYYY-MM-DD-NNNN.md(例:2026-09-25-0354.md)' };
|
|
4699
|
+
}
|
|
4700
|
+
const oldBase = path.basename(target.fp);
|
|
4701
|
+
if (name === oldBase) return { error: true, text: '新文件名与原名相同,无需改名。' };
|
|
4702
|
+
const owner = ownerFromPrivatePath(target.root, target.fp);
|
|
4703
|
+
let meta;
|
|
4704
|
+
try { meta = parseFrontmatter(readMemoryText(target.root, target.fp, owner)).meta; }
|
|
4705
|
+
catch (e) { return { error: true, text: '读取失败: ' + e.message }; }
|
|
4706
|
+
const type = (meta.type || 'FACT').toUpperCase();
|
|
4707
|
+
const conflicts = [];
|
|
4708
|
+
for (const fp of collectEntryFiles(target.root)) {
|
|
4709
|
+
if (fp === target.fp) continue;
|
|
4710
|
+
if (path.basename(fp) !== name) continue;
|
|
4711
|
+
const otherOwner = ownerFromPrivatePath(target.root, fp);
|
|
4712
|
+
if (otherOwner !== owner) continue;
|
|
4713
|
+
let otherMeta;
|
|
4714
|
+
try { otherMeta = parseFrontmatter(readMemoryText(target.root, fp, otherOwner)).meta; }
|
|
4715
|
+
catch (e) { conflicts.push(relOf(target.root, fp)); continue; }
|
|
4716
|
+
if ((otherMeta.type || 'FACT').toUpperCase() === type) conflicts.push(relOf(target.root, fp));
|
|
4717
|
+
}
|
|
4718
|
+
if (conflicts.length) {
|
|
4719
|
+
return { error: true, text: '目标文件名已被同 owner 同类型记忆占用,拒绝改名(跨平铺 / 分层检查): ' + conflicts.join('、') };
|
|
4720
|
+
}
|
|
4721
|
+
const dest = path.join(path.dirname(target.fp), name);
|
|
4722
|
+
if (fs.existsSync(dest)) return { error: true, text: '目标文件已存在,拒绝覆盖: ' + relOf(target.root, dest) };
|
|
4723
|
+
const destRel = relOf(target.root, dest);
|
|
4724
|
+
if (opts.dryRun) {
|
|
4725
|
+
return { error: false, text: '预览(未改动): ' + target.rel + ' → ' + destRel };
|
|
4726
|
+
}
|
|
4727
|
+
const guard = destructiveGuardCore({
|
|
4728
|
+
root: target.root,
|
|
4729
|
+
action: 'rename',
|
|
4730
|
+
snapshotDir: opts.snapshotDir,
|
|
4731
|
+
backupDir: opts.backupDir,
|
|
4732
|
+
allowSameVolumeForTest: opts.allowSameVolumeForTest,
|
|
4733
|
+
sameVolumeFn: opts.sameVolumeFn,
|
|
4734
|
+
now: opts.now,
|
|
4735
|
+
});
|
|
4736
|
+
if (guard.error) return { error: true, text: guard.text };
|
|
4737
|
+
try {
|
|
4738
|
+
fs.renameSync(target.fp, dest);
|
|
4739
|
+
} catch (e) {
|
|
4740
|
+
return { error: true, text: '改名失败,原文件未动: ' + e.message };
|
|
4741
|
+
}
|
|
4742
|
+
try {
|
|
4743
|
+
removeIndexEntry(target.root, target.rel);
|
|
4744
|
+
buildIndex(target.root);
|
|
4745
|
+
} catch (e) { /* 索引失败不回滚文件改名;下一次 reindex 会重建 */ }
|
|
4746
|
+
appendAudit(target.root, 'audit', {
|
|
4747
|
+
ts: new Date().toISOString(),
|
|
4748
|
+
file: target.rel,
|
|
4749
|
+
action: 'rename',
|
|
4750
|
+
to: destRel,
|
|
4751
|
+
reason: opts.reason ? String(opts.reason) : '',
|
|
4752
|
+
});
|
|
4753
|
+
return { error: false, text: '已改名: ' + target.rel + ' → ' + destRel + '\n事务快照: ' + guard.snapshot.id };
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4459
4756
|
function forgetCore(fileRef, opts) {
|
|
4460
4757
|
opts = opts || {};
|
|
4461
4758
|
const selfAgent = resolveIdentity(opts).id;
|
|
@@ -4499,28 +4796,200 @@ function forgetCore(fileRef, opts) {
|
|
|
4499
4796
|
}
|
|
4500
4797
|
return { error: false, text: '已移入回收区: ' + trashFile };
|
|
4501
4798
|
}
|
|
4799
|
+
// ---- v0.18.0 A3:容量水位 / 淘汰候选 / 晋升建议(只读报告,不自动动数据)----
|
|
4800
|
+
const CAPACITY_CONFIG_DEFAULTS = {
|
|
4801
|
+
capacity_warn_bytes: 50 * 1024 * 1024,
|
|
4802
|
+
capacity_cooldown_days: 30,
|
|
4803
|
+
capacity_candidate_limit: 20,
|
|
4804
|
+
promotion_min_hits: 3,
|
|
4805
|
+
promotion_min_queries: 3,
|
|
4806
|
+
};
|
|
4807
|
+
function capacityNumber(cfg, key) {
|
|
4808
|
+
const raw = cfg ? cfg[key] : undefined;
|
|
4809
|
+
if (raw === undefined || raw === null || raw === '') return CAPACITY_CONFIG_DEFAULTS[key];
|
|
4810
|
+
const n = parseFloat(raw);
|
|
4811
|
+
return (n >= 0) ? n : CAPACITY_CONFIG_DEFAULTS[key];
|
|
4812
|
+
}
|
|
4813
|
+
function isEvergreenTagged(meta) {
|
|
4814
|
+
// tags 在真实库里可能是 JSON 数组串(["pinned"]),parseTags 会保留引号,这里统一去掉再比较。
|
|
4815
|
+
const tags = parseTags(meta && meta.tags).map(function (t) {
|
|
4816
|
+
return String(t).toLowerCase().replace(/^["']|["']$/g, '');
|
|
4817
|
+
});
|
|
4818
|
+
return tags.indexOf('evergreen') !== -1 || tags.indexOf('pinned') !== -1;
|
|
4819
|
+
}
|
|
4820
|
+
function capacityMemoryBytes(root) {
|
|
4821
|
+
let total = 0;
|
|
4822
|
+
for (const fp of collectEntryFiles(root)) {
|
|
4823
|
+
try { total += fs.statSync(fp).size; } catch (e) { /* 单文件缺失不影响整体水位 */ }
|
|
4824
|
+
}
|
|
4825
|
+
return total;
|
|
4826
|
+
}
|
|
4827
|
+
function capacityCandidateRow(e) {
|
|
4828
|
+
return {
|
|
4829
|
+
file: e.file,
|
|
4830
|
+
type: e.type,
|
|
4831
|
+
subject: e.subject,
|
|
4832
|
+
hits_30d: e.hits_30d,
|
|
4833
|
+
hits_90d: e.hits_90d,
|
|
4834
|
+
last_used: e.last_used,
|
|
4835
|
+
age_days: e.age_days,
|
|
4836
|
+
utility: e.utility,
|
|
4837
|
+
suggested_command: 'yotta-memory explain ' + e.file,
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
function capacityCore(opts) {
|
|
4841
|
+
opts = opts || {};
|
|
4842
|
+
const root = path.resolve(opts.root || userRoot());
|
|
4843
|
+
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。', report: null };
|
|
4844
|
+
const cfg = loadConfig();
|
|
4845
|
+
const selfAgent = (opts.selfAgent !== undefined) ? opts.selfAgent : resolveIdentity(opts).id;
|
|
4846
|
+
const cooldownDays = capacityNumber(cfg, 'capacity_cooldown_days');
|
|
4847
|
+
const limit = (opts.limit !== undefined && opts.limit !== null)
|
|
4848
|
+
? (parseInt(opts.limit, 10) || 0)
|
|
4849
|
+
: capacityNumber(cfg, 'capacity_candidate_limit');
|
|
4850
|
+
const minHits = capacityNumber(cfg, 'promotion_min_hits');
|
|
4851
|
+
const minQueries = capacityNumber(cfg, 'promotion_min_queries');
|
|
4852
|
+
const skipped = { cooldown: 0, evergreen: 0, denied: 0, unreadable: 0 };
|
|
4853
|
+
const activityBase = [];
|
|
4854
|
+
const candidatesForCleanup = [];
|
|
4855
|
+
for (const fp of collectEntryFiles(root)) {
|
|
4856
|
+
const r = relOf(root, fp);
|
|
4857
|
+
if (checkOwnerWritable(root, r, selfAgent, opts.unsafe)) { skipped.denied++; continue; }
|
|
4858
|
+
let e;
|
|
4859
|
+
try { e = readEntry(fp, root); } catch (err) { skipped.unreadable++; continue; }
|
|
4860
|
+
e.hits_30d = usageTotals(e, 30);
|
|
4861
|
+
e.hits_90d = usageTotals(e, 90);
|
|
4862
|
+
e.last_used = e.last_accessed || e.created || '';
|
|
4863
|
+
e.age_days = e.created ? daysBetween(e.created, today()) : -1;
|
|
4864
|
+
e.utility = round3(utilityScore(e.meta));
|
|
4865
|
+
activityBase.push(e);
|
|
4866
|
+
if (e.immutable || e.type === 'BOUND' || isEvergreenTagged(e.meta)) { skipped.evergreen++; continue; }
|
|
4867
|
+
if (e.age_days >= 0 && e.age_days < cooldownDays) { skipped.cooldown++; continue; }
|
|
4868
|
+
candidatesForCleanup.push(e);
|
|
4869
|
+
}
|
|
4870
|
+
const byOldest = function (a, b) {
|
|
4871
|
+
const la = a.last_used || '', lb = b.last_used || '';
|
|
4872
|
+
if (la !== lb) return la.localeCompare(lb);
|
|
4873
|
+
return String(a.file).localeCompare(String(b.file));
|
|
4874
|
+
};
|
|
4875
|
+
const lru = candidatesForCleanup.slice().sort(byOldest).slice(0, limit).map(capacityCandidateRow);
|
|
4876
|
+
const lfu = candidatesForCleanup.slice().sort(function (a, b) {
|
|
4877
|
+
if (a.hits_90d !== b.hits_90d) return a.hits_90d - b.hits_90d;
|
|
4878
|
+
return byOldest(a, b);
|
|
4879
|
+
}).slice(0, limit).map(capacityCandidateRow);
|
|
4880
|
+
const promotions = candidatesForCleanup.filter(function (e) {
|
|
4881
|
+
if (e.hits_90d < minHits) return false;
|
|
4882
|
+
if (Object.keys(parseHitQueries(e.hit_queries)).length < minQueries) return false;
|
|
4883
|
+
if (e.weight >= 1.5) return false;
|
|
4884
|
+
if (isConsolidatedSummary(e)) return false;
|
|
4885
|
+
return true;
|
|
4886
|
+
}).map(function (e) {
|
|
4887
|
+
const row = capacityCandidateRow(e);
|
|
4888
|
+
row.distinct_queries = Object.keys(parseHitQueries(e.hit_queries)).length;
|
|
4889
|
+
row.suggested_command = 'yotta-memory feedback ' + e.file + ' --useful';
|
|
4890
|
+
return row;
|
|
4891
|
+
});
|
|
4892
|
+
const scale = scaleReport(root, cfg);
|
|
4893
|
+
const memoryBytes = capacityMemoryBytes(root);
|
|
4894
|
+
const warnBytes = capacityNumber(cfg, 'capacity_warn_bytes');
|
|
4895
|
+
const infoBytes = warnBytes * 0.6;
|
|
4896
|
+
const memoryLevel = memoryBytes > warnBytes ? 'warning' : (memoryBytes > infoBytes ? 'info' : 'ok');
|
|
4897
|
+
const waterLevel = (scale.level === 'warning' || memoryLevel === 'warning')
|
|
4898
|
+
? 'warning'
|
|
4899
|
+
: ((scale.level === 'info' || memoryLevel === 'info') ? 'info' : 'ok');
|
|
4900
|
+
const thresholds = Object.assign({}, scale.thresholds, {
|
|
4901
|
+
capacity_warn_bytes: warnBytes,
|
|
4902
|
+
capacity_cooldown_days: cooldownDays,
|
|
4903
|
+
capacity_candidate_limit: limit,
|
|
4904
|
+
promotion_min_hits: minHits,
|
|
4905
|
+
promotion_min_queries: minQueries,
|
|
4906
|
+
});
|
|
4907
|
+
const water = {
|
|
4908
|
+
entries: scale.entries,
|
|
4909
|
+
files_per_dir: scale.files_per_dir,
|
|
4910
|
+
index_bytes: scale.index_bytes,
|
|
4911
|
+
cold_start_ms: scale.cold_start_ms,
|
|
4912
|
+
memory_bytes: memoryBytes,
|
|
4913
|
+
thresholds: thresholds,
|
|
4914
|
+
metrics: scale.metrics.concat([{
|
|
4915
|
+
key: 'memory_bytes', value: memoryBytes, unit: '字节', info: infoBytes, warn: warnBytes, level: memoryLevel,
|
|
4916
|
+
}]),
|
|
4917
|
+
level: waterLevel,
|
|
4918
|
+
};
|
|
4919
|
+
const activity = {
|
|
4920
|
+
readable: activityBase.length,
|
|
4921
|
+
hits_30d_entries: activityBase.filter(function (e) { return e.hits_30d > 0; }).length,
|
|
4922
|
+
hits_90d_entries: activityBase.filter(function (e) { return e.hits_90d > 0; }).length,
|
|
4923
|
+
never_hit: activityBase.filter(function (e) { return e.hits_90d === 0 && (parseInt(e.access_count, 10) || 0) === 0; }).length,
|
|
4924
|
+
top: activityBase.filter(function (e) { return e.hits_90d > 0; })
|
|
4925
|
+
.sort(function (a, b) {
|
|
4926
|
+
if (b.hits_90d !== a.hits_90d) return b.hits_90d - a.hits_90d;
|
|
4927
|
+
return byOldest(a, b);
|
|
4928
|
+
})
|
|
4929
|
+
.slice(0, Math.min(limit, 10))
|
|
4930
|
+
.map(capacityCandidateRow),
|
|
4931
|
+
};
|
|
4932
|
+
const lines = ['## yotta-memory maintain --capacity(容量水位)'];
|
|
4933
|
+
const waterLabel = waterLevel === 'warning' ? '告警' : (waterLevel === 'info' ? '提示' : '正常');
|
|
4934
|
+
lines.push('- 水位: ' + waterLabel
|
|
4935
|
+
+ '(条目 ' + scale.entries + ' / 记忆文件 ' + memoryBytes + ' 字节(阈值 ' + warnBytes + ')'
|
|
4936
|
+
+ ' / 索引 ' + scale.index_bytes + ' 字节 / 单目录最大 ' + scale.files_per_dir + ' 个文件 / 冷启动 ' + scale.cold_start_ms + 'ms)');
|
|
4937
|
+
lines.push('- 活跃度: 可读 ' + activity.readable + ' 条;30 天命中 ' + activity.hits_30d_entries
|
|
4938
|
+
+ ' 条 / 90 天命中 ' + activity.hits_90d_entries + ' 条 / 无命中记录 ' + activity.never_hit + ' 条');
|
|
4939
|
+
lines.push('');
|
|
4940
|
+
lines.push('### LRU 淘汰候选(最久未用,' + lru.length + ' 条)');
|
|
4941
|
+
if (!lru.length) lines.push('(无)');
|
|
4942
|
+
for (const row of lru) {
|
|
4943
|
+
lines.push('- [' + row.type + '] ' + row.file + '(90 天命中 ' + row.hits_90d + ',' + row.age_days + ' 天龄,utility ' + row.utility + ')— ' + row.subject);
|
|
4944
|
+
}
|
|
4945
|
+
lines.push('');
|
|
4946
|
+
lines.push('### LFU 淘汰候选(命中最少,' + lfu.length + ' 条)');
|
|
4947
|
+
if (!lfu.length) lines.push('(无)');
|
|
4948
|
+
for (const row of lfu) {
|
|
4949
|
+
lines.push('- [' + row.type + '] ' + row.file + '(90 天命中 ' + row.hits_90d + ',最久未用 ' + row.last_used + ')— ' + row.subject);
|
|
4950
|
+
}
|
|
4951
|
+
lines.push('');
|
|
4952
|
+
lines.push('### 晋升建议(只出建议,不自动改权重,' + promotions.length + ' 条)');
|
|
4953
|
+
if (!promotions.length) lines.push('(无:需要 90 天命中 ≥ ' + minHits + ' 且不同查询 ≥ ' + minQueries + ' 且 weight < 1.5)');
|
|
4954
|
+
for (const row of promotions) {
|
|
4955
|
+
lines.push('- ' + row.file + '(90 天命中 ' + row.hits_90d + ',不同查询 ' + row.distinct_queries + ')→ ' + row.suggested_command);
|
|
4956
|
+
}
|
|
4957
|
+
lines.push('');
|
|
4958
|
+
lines.push('### 跳过(不进候选)');
|
|
4959
|
+
lines.push('- 冷却期(<' + cooldownDays + ' 天): ' + skipped.cooldown + ' 条 / 常青(immutable、BOUND、evergreen、pinned): '
|
|
4960
|
+
+ skipped.evergreen + ' 条 / 其它 owner: ' + skipped.denied + ' 条 / 无法读取: ' + skipped.unreadable + ' 条');
|
|
4961
|
+
lines.push('- 本命令只读:不改记忆、不改权重;归档请显式执行 yotta-memory archive。');
|
|
4962
|
+
const report = {
|
|
4963
|
+
schemaVersion: 1,
|
|
4964
|
+
root: root,
|
|
4965
|
+
thresholds: thresholds,
|
|
4966
|
+
water: water,
|
|
4967
|
+
activity: activity,
|
|
4968
|
+
candidates: { lru: lru, lfu: lfu },
|
|
4969
|
+
promotions: promotions,
|
|
4970
|
+
skipped: skipped,
|
|
4971
|
+
};
|
|
4972
|
+
return { error: false, text: lines.join('\n'), report: report };
|
|
4973
|
+
}
|
|
4502
4974
|
function archiveCore(opts) {
|
|
4503
4975
|
opts = opts || {};
|
|
4504
4976
|
const cfg = loadConfig();
|
|
4505
4977
|
const days = opts.days || parseInt(cfg.maintain_archived_age || '180', 10) || 180;
|
|
4506
4978
|
const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : (parseFloat(cfg.maintain_archived_utility || 0.35));
|
|
4979
|
+
const cooldownDays = capacityNumber(cfg, 'capacity_cooldown_days');
|
|
4507
4980
|
const root = userRoot();
|
|
4508
4981
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
4509
|
-
const
|
|
4510
|
-
|
|
4511
|
-
action: 'archive',
|
|
4512
|
-
snapshotDir: opts.snapshotDir,
|
|
4513
|
-
backupDir: opts.backupDir,
|
|
4514
|
-
allowSameVolumeForTest: opts.allowSameVolumeForTest,
|
|
4515
|
-
sameVolumeFn: opts.sameVolumeFn,
|
|
4516
|
-
now: opts.now,
|
|
4517
|
-
});
|
|
4518
|
-
if (guard.error) return { error: true, text: guard.text };
|
|
4982
|
+
const dryRun = !!opts.dryRun;
|
|
4983
|
+
const force = !!opts.force;
|
|
4519
4984
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
4520
|
-
let moved = 0;
|
|
4521
|
-
let deniedCrossOwner = 0;
|
|
4522
|
-
const movedFiles = [];
|
|
4523
4985
|
const selfAgent = resolveIdentity(opts).id;
|
|
4986
|
+
// v0.18.0 修正:先只读扫描候选与跳过统计,再决定要不要过破坏性闸门 ——
|
|
4987
|
+
// 预演(--dry-run)与「没有候选」两种情况下都不写盘。
|
|
4988
|
+
// 旧行为是进门就建整库快照 + 写审计,且 --dry-run 标志被静默吞掉(照常归档)。
|
|
4989
|
+
const candidates = [];
|
|
4990
|
+
let deniedCrossOwner = 0;
|
|
4991
|
+
let skippedCooldown = 0;
|
|
4992
|
+
let skippedEvergreen = 0;
|
|
4524
4993
|
for (const fp of collectEntryFiles(root)) {
|
|
4525
4994
|
const owner = ownerFromPrivatePath(root, fp);
|
|
4526
4995
|
// v0.17.2:只归档自己有权写的条目 —— 旧行为遍历全部 owner,
|
|
@@ -4537,23 +5006,86 @@ function archiveCore(opts) {
|
|
|
4537
5006
|
if (isNaN(createdTs)) continue;
|
|
4538
5007
|
const t = (meta.type || 'FACT').toUpperCase();
|
|
4539
5008
|
if (t === 'BOUND') continue; // v0.10.0 BOUND 豁免归档(边界常驻)
|
|
5009
|
+
if (!force && isEvergreenTagged(meta)) { skippedEvergreen++; continue; }
|
|
5010
|
+
const ageDays = meta.created ? daysBetween(meta.created, today()) : -1;
|
|
5011
|
+
if (!force && ageDays >= 0 && ageDays < cooldownDays) { skippedCooldown++; continue; }
|
|
4540
5012
|
// v0.8.0 统一效用分(盖棺分)替代 vitality
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
5013
|
+
const score = utilityScore(meta);
|
|
5014
|
+
if (score < threshold && createdTs < cutoff) {
|
|
5015
|
+
candidates.push({
|
|
5016
|
+
fp: fp,
|
|
5017
|
+
owner: owner,
|
|
5018
|
+
entry: {
|
|
5019
|
+
file: relOf(root, fp),
|
|
5020
|
+
type: t,
|
|
5021
|
+
subject: String(meta.subject || ''),
|
|
5022
|
+
age_days: ageDays,
|
|
5023
|
+
utility: round3(score),
|
|
5024
|
+
},
|
|
5025
|
+
});
|
|
4549
5026
|
}
|
|
4550
5027
|
}
|
|
4551
|
-
|
|
4552
|
-
let text = '已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) + '\n事务快照: ' + guard.snapshot.id;
|
|
5028
|
+
const skippedLines = [];
|
|
4553
5029
|
if (deniedCrossOwner) {
|
|
4554
|
-
|
|
5030
|
+
skippedLines.push('跨 owner 私密条目已跳过(' + deniedCrossOwner + ' 条):请用 --agent <id> 声明自己的身份;只有用户显式授权(--unsafe)才能越界维护。');
|
|
4555
5031
|
}
|
|
4556
|
-
|
|
5032
|
+
if (skippedCooldown || skippedEvergreen) {
|
|
5033
|
+
skippedLines.push('因冷却期 / 常青豁免跳过 ' + (skippedCooldown + skippedEvergreen) + ' 条(冷却期 ' + skippedCooldown + ' / 常青 ' + skippedEvergreen + ');确认要一并归档时加 --force(immutable 与 BOUND 仍豁免)。');
|
|
5034
|
+
}
|
|
5035
|
+
const report = {
|
|
5036
|
+
schemaVersion: 1,
|
|
5037
|
+
root: root,
|
|
5038
|
+
mode: dryRun ? 'dry-run' : 'apply',
|
|
5039
|
+
days: days,
|
|
5040
|
+
threshold: threshold,
|
|
5041
|
+
cooldown_days: cooldownDays,
|
|
5042
|
+
force: force,
|
|
5043
|
+
candidates: candidates.map(function (c) { return c.entry; }),
|
|
5044
|
+
archived: [],
|
|
5045
|
+
skipped: { cooldown: skippedCooldown, evergreen: skippedEvergreen, cross_owner: deniedCrossOwner },
|
|
5046
|
+
};
|
|
5047
|
+
if (dryRun) {
|
|
5048
|
+
// 预演只读:不动文件、不建事务快照、不写审计、不改索引。
|
|
5049
|
+
const lines = ['预览(未改动): 将归档 ' + candidates.length + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR)];
|
|
5050
|
+
lines.push('- 判定: 效用 < ' + threshold + ' 且未使用 > ' + days + ' 天' + (force
|
|
5051
|
+
? '(--force:冷却期与常青标签不再豁免)'
|
|
5052
|
+
: '(冷却期 ' + cooldownDays + ' 天内与 immutable / BOUND / evergreen / pinned 豁免)'));
|
|
5053
|
+
for (const c of candidates) {
|
|
5054
|
+
lines.push('- [' + c.entry.type + '] ' + c.entry.file + '(' + c.entry.age_days + ' 天龄,utility ' + c.entry.utility + ')— ' + c.entry.subject);
|
|
5055
|
+
}
|
|
5056
|
+
if (!candidates.length) lines.push('(无符合条件的记忆)');
|
|
5057
|
+
lines.push('- 预演只读:未创建事务快照、未写审计、未移动文件;去掉 --dry-run 才执行(执行前先过破坏性写入闸门:doctor + 独立备份目录 + 异卷)。');
|
|
5058
|
+
lines.push.apply(lines, skippedLines);
|
|
5059
|
+
return { error: false, dryRun: true, text: lines.join('\n'), report: report };
|
|
5060
|
+
}
|
|
5061
|
+
if (!candidates.length) {
|
|
5062
|
+
// 无候选 = 无写入:不创建整库事务快照,也不写审计。
|
|
5063
|
+
const lines = ['已归档 0 条旧记忆到 ' + path.join(root, ARCHIVE_DIR), '没有符合条件的记忆:未创建事务快照(无写入)。'];
|
|
5064
|
+
lines.push.apply(lines, skippedLines);
|
|
5065
|
+
return { error: false, text: lines.join('\n'), report: report };
|
|
5066
|
+
}
|
|
5067
|
+
const guard = destructiveGuardCore({
|
|
5068
|
+
root: root,
|
|
5069
|
+
action: 'archive',
|
|
5070
|
+
snapshotDir: opts.snapshotDir,
|
|
5071
|
+
backupDir: opts.backupDir,
|
|
5072
|
+
allowSameVolumeForTest: opts.allowSameVolumeForTest,
|
|
5073
|
+
sameVolumeFn: opts.sameVolumeFn,
|
|
5074
|
+
now: opts.now,
|
|
5075
|
+
});
|
|
5076
|
+
if (guard.error) return { error: true, text: guard.text };
|
|
5077
|
+
const movedFiles = [];
|
|
5078
|
+
for (const c of candidates) {
|
|
5079
|
+
const dest = path.join(root, archiveRelFor(root, c.entry.file, c.entry.type, c.owner));
|
|
5080
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
5081
|
+
fs.renameSync(c.fp, dest);
|
|
5082
|
+
movedFiles.push(c.entry.file);
|
|
5083
|
+
}
|
|
5084
|
+
if (movedFiles.length) removeIndexEntries(root, movedFiles);
|
|
5085
|
+
report.archived = movedFiles;
|
|
5086
|
+
let text = '已归档 ' + movedFiles.length + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) + '\n事务快照: ' + guard.snapshot.id;
|
|
5087
|
+
if (skippedLines.length) text += '\n' + skippedLines.join('\n');
|
|
5088
|
+
return { error: false, text: text, report: report };
|
|
4557
5089
|
}
|
|
4558
5090
|
|
|
4559
5091
|
// ---- v0.8.0 自我学习/自我进化/自我提升:feedback / explain / maintain / distill ----
|
|
@@ -4615,6 +5147,10 @@ function feedbackCore(ref, opts) {
|
|
|
4615
5147
|
const before = { weight: round3(weight), confidence: round3(confidence), feedback_net: fb };
|
|
4616
5148
|
const after = { weight: round3(nw), confidence: round3(nc), feedback_net: nfb };
|
|
4617
5149
|
const patch = { weight: after.weight, confidence: after.confidence, feedback_net: after.feedback_net, updated: today() };
|
|
5150
|
+
if (useful) {
|
|
5151
|
+
const usageCfg = usageSettings(opts);
|
|
5152
|
+
if (usageCfg.enabled) Object.assign(patch, usagePatchForMeta(meta, 'use', '', usageCfg, today()));
|
|
5153
|
+
}
|
|
4618
5154
|
rewriteFrontmatter(target.fp, patch, target.root, owner);
|
|
4619
5155
|
upsertIndexEntry(target.root, readEntry(target.fp, target.root));
|
|
4620
5156
|
appendAudit(target.root, 'feedback', { ts: new Date().toISOString(), file: target.rel, action: useful ? 'useful' : 'useless', before: before, after: after, reason: opts.reason || '' });
|
|
@@ -4658,6 +5194,7 @@ function explainCore(ref, opts) {
|
|
|
4658
5194
|
// v0.17.2:跨 owner 私密条目 fail-closed —— 旧行为只是加一句「仅元数据」,
|
|
4659
5195
|
// 正文(subject / statement)照样输出,等于把别人的私密记忆读给调用方。
|
|
4660
5196
|
if (deny) return { error: true, text: deny };
|
|
5197
|
+
applyUsageHits(target.root, [target.rel], 'get', '', { selfAgent: selfAgent, noUsage: !!opts.noUsage });
|
|
4661
5198
|
const ub = utilityBreakdown(entry);
|
|
4662
5199
|
const ageDays = entry.created ? daysBetween(entry.created, today()) : 0;
|
|
4663
5200
|
const cfg = loadConfig();
|
|
@@ -6081,6 +6618,13 @@ function cmdExplain(ref, opts) {
|
|
|
6081
6618
|
if (r.error) process.exit(2);
|
|
6082
6619
|
}
|
|
6083
6620
|
function cmdMaintain(opts) {
|
|
6621
|
+
if (opts.capacity) {
|
|
6622
|
+
const r = capacityCore(opts);
|
|
6623
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
6624
|
+
else console.log(r.text);
|
|
6625
|
+
if (r.error) process.exit(2);
|
|
6626
|
+
return;
|
|
6627
|
+
}
|
|
6084
6628
|
if (opts.merge) {
|
|
6085
6629
|
const parts = String(opts.merge).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
6086
6630
|
if (parts.length < 2) { console.error('--merge 需要两个文件,逗号分隔'); process.exit(2); }
|
|
@@ -6111,9 +6655,27 @@ function cmdForget(fileRef, opts) {
|
|
|
6111
6655
|
console.log(r.text);
|
|
6112
6656
|
if (r.error) process.exit(2);
|
|
6113
6657
|
}
|
|
6658
|
+
|
|
6659
|
+
function cmdRename(fileRef, newName, opts) {
|
|
6660
|
+
const ident = resolveIdentity(opts);
|
|
6661
|
+
if (ident.error) {
|
|
6662
|
+
console.log(ident.error);
|
|
6663
|
+
process.exit(3);
|
|
6664
|
+
}
|
|
6665
|
+
const r = renameCore(fileRef, newName, {
|
|
6666
|
+
selfAgent: ident.id,
|
|
6667
|
+
unsafe: !!(opts && opts.unsafe),
|
|
6668
|
+
dryRun: !!(opts && opts.dryRun),
|
|
6669
|
+
reason: opts && opts.reason,
|
|
6670
|
+
});
|
|
6671
|
+
console.log(r.text);
|
|
6672
|
+
if (r.error) process.exit(2);
|
|
6673
|
+
}
|
|
6114
6674
|
function cmdArchive(opts) {
|
|
6115
6675
|
const r = archiveCore(opts);
|
|
6116
|
-
console.log(r.
|
|
6676
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
6677
|
+
else console.log(r.text);
|
|
6678
|
+
if (r.error) process.exit(2);
|
|
6117
6679
|
}
|
|
6118
6680
|
function cmdBackupCreate(opts) {
|
|
6119
6681
|
const r = backupCreateCore(opts);
|
|
@@ -6631,6 +7193,7 @@ function contextCore(opts) {
|
|
|
6631
7193
|
opts = opts || {};
|
|
6632
7194
|
const roots = memoryRoots();
|
|
6633
7195
|
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
7196
|
+
if (opts.audit) return contextAuditCore(opts);
|
|
6634
7197
|
const root = userRoot();
|
|
6635
7198
|
const limit = parseInt(opts.limit || '10', 10) || 10;
|
|
6636
7199
|
const ident = resolveIdentity(opts);
|
|
@@ -6658,6 +7221,7 @@ function contextCore(opts) {
|
|
|
6658
7221
|
const trace = [];
|
|
6659
7222
|
const lines = [];
|
|
6660
7223
|
const shownFiles = new Set();
|
|
7224
|
+
const usedFiles = new Set();
|
|
6661
7225
|
const FENCE = String.fromCharCode(96, 96, 96);
|
|
6662
7226
|
function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
|
|
6663
7227
|
function appendContextEntry(entry, reason, options) {
|
|
@@ -6674,6 +7238,7 @@ function contextCore(opts) {
|
|
|
6674
7238
|
}
|
|
6675
7239
|
lines.push(line);
|
|
6676
7240
|
shownFiles.add(entry.file);
|
|
7241
|
+
usedFiles.add(entry.file);
|
|
6677
7242
|
trace.push('[included] ' + entry.file + ' reason: ' + reason);
|
|
6678
7243
|
return true;
|
|
6679
7244
|
}
|
|
@@ -6755,7 +7320,8 @@ function contextCore(opts) {
|
|
|
6755
7320
|
selfAgent: selfAgent,
|
|
6756
7321
|
years: years,
|
|
6757
7322
|
embedding: embeddingCommand,
|
|
6758
|
-
embeddingTimeout: embeddingTimeout
|
|
7323
|
+
embeddingTimeout: embeddingTimeout,
|
|
7324
|
+
noUsage: true
|
|
6759
7325
|
});
|
|
6760
7326
|
const focusedEntries = focused.entries || [];
|
|
6761
7327
|
if (!focusedEntries.length) lines.push('(无匹配记忆)');
|
|
@@ -6822,6 +7388,14 @@ function contextCore(opts) {
|
|
|
6822
7388
|
if (!trace.length) lines.push('(无选择记录)');
|
|
6823
7389
|
for (const t of trace) lines.push(t);
|
|
6824
7390
|
}
|
|
7391
|
+
// v0.18.0 A3:纳入上下文包的条目记 use 信号(本次调用内按文件去重;--no-usage 可关)。
|
|
7392
|
+
if (usedFiles.size) {
|
|
7393
|
+
const usageOpts = { selfAgent: selfAgent, noUsage: !!opts.noUsage };
|
|
7394
|
+
for (const r of roots) {
|
|
7395
|
+
const rels = Array.from(usedFiles).filter(function (rel) { return fs.existsSync(path.join(r, rel)); });
|
|
7396
|
+
if (rels.length) applyUsageHits(r, rels, 'use', '', usageOpts);
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
6825
7399
|
const reliability = doctorCore({ root: root });
|
|
6826
7400
|
if (reliability.level !== 'ok') {
|
|
6827
7401
|
lines.push('');
|
|
@@ -6831,11 +7405,12 @@ function contextCore(opts) {
|
|
|
6831
7405
|
for (const message of reliability.warnings) lines.push('- [警告] ' + message);
|
|
6832
7406
|
if (!reliability.ok) lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
|
|
6833
7407
|
}
|
|
6834
|
-
return { error: false, exitCode: 0, text: lines.join('\n') };
|
|
7408
|
+
return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace };
|
|
6835
7409
|
}
|
|
6836
7410
|
function cmdContext(opts) {
|
|
6837
7411
|
const r = contextCore(opts);
|
|
6838
|
-
console.log(r.
|
|
7412
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
7413
|
+
else console.log(r.text);
|
|
6839
7414
|
if (r.error) {
|
|
6840
7415
|
const reminder = migrationReminderText(userRoot());
|
|
6841
7416
|
if (reminder) console.log('\n' + reminder);
|
|
@@ -6843,6 +7418,221 @@ function cmdContext(opts) {
|
|
|
6843
7418
|
if (r.exitCode) process.exit(r.exitCode);
|
|
6844
7419
|
}
|
|
6845
7420
|
|
|
7421
|
+
// ---- v0.18.0 A7:context --audit 上下文压缩审计(只读;不自动补写)----
|
|
7422
|
+
function contextAuditGate(opts) {
|
|
7423
|
+
const raw = Array.isArray(opts.gate) ? opts.gate[opts.gate.length - 1] : opts.gate;
|
|
7424
|
+
if (raw === undefined || raw === null || raw === '') return null;
|
|
7425
|
+
const n = parseInt(raw, 10);
|
|
7426
|
+
return isNaN(n) ? null : n;
|
|
7427
|
+
}
|
|
7428
|
+
function contextAuditReadStdin() {
|
|
7429
|
+
// 只在 fd 0 明确是管道 / socket 时读取,避免测试运行器或交互终端被阻塞。
|
|
7430
|
+
try {
|
|
7431
|
+
const st = fs.fstatSync(0);
|
|
7432
|
+
if (!st.isFIFO() && !st.isSocket()) return null;
|
|
7433
|
+
return fs.readFileSync(0, 'utf8');
|
|
7434
|
+
} catch (e) {
|
|
7435
|
+
return null;
|
|
7436
|
+
}
|
|
7437
|
+
}
|
|
7438
|
+
function contextAuditSegments(text) {
|
|
7439
|
+
const out = [];
|
|
7440
|
+
// 中文信号词为主;补英文决策词,保证中英混排的宿主压缩摘要也能切出候选句。
|
|
7441
|
+
const keywordRe = /决定|敲定|约定|口径|红线|规则|承诺|下一步|待办|遗留|注意|以后|必须|不要|decision|decided|agreement|policy|rule|commit|next step|todo|remaining|note|must|never/i;
|
|
7442
|
+
const sectionRe = /^#{1,6}\s*(决策|下一步|遗留问题|decisions?|next\s*steps?|remaining|todos?)/i;
|
|
7443
|
+
let inSection = false;
|
|
7444
|
+
let inCode = false;
|
|
7445
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
7446
|
+
const line = raw.trim();
|
|
7447
|
+
if (/^```/.test(line)) { inCode = !inCode; continue; }
|
|
7448
|
+
if (inCode || !line) continue;
|
|
7449
|
+
if (/^#{1,6}\s+/.test(line)) { inSection = sectionRe.test(line); continue; }
|
|
7450
|
+
if (/^\|/.test(line)) continue;
|
|
7451
|
+
const item = line.replace(/^[-*+]\s+/, '').replace(/^\d+[.、)]\s*/, '').trim();
|
|
7452
|
+
if (item.length < 4) continue;
|
|
7453
|
+
if (inSection || keywordRe.test(item)) {
|
|
7454
|
+
if (out.indexOf(item) === -1) out.push(item);
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
return out;
|
|
7458
|
+
}
|
|
7459
|
+
function contextAuditCoverage(candidate, entries, minCoverage) {
|
|
7460
|
+
for (const e of entries) {
|
|
7461
|
+
const corpus = String(e.subject || '') + ' ' + String(e.statement || '');
|
|
7462
|
+
if (corpus.indexOf(candidate) !== -1) return { entry: e, coverage: 1, best: e };
|
|
7463
|
+
const stmt = String(e.statement || '');
|
|
7464
|
+
if (stmt.length >= 6 && candidate.indexOf(stmt) !== -1) return { entry: e, coverage: 1, best: e };
|
|
7465
|
+
}
|
|
7466
|
+
const tokens = new Set(tokenize(candidate).filter(function (t) { return t.indexOf(':') === -1; }));
|
|
7467
|
+
if (!tokens.size) return { entry: null, coverage: 0, best: null };
|
|
7468
|
+
let best = null;
|
|
7469
|
+
let bestCoverage = 0;
|
|
7470
|
+
for (const e of entries) {
|
|
7471
|
+
const etoks = new Set(tokenize(String(e.subject || '') + ' ' + String(e.statement || '')).filter(function (t) { return t.indexOf(':') === -1; }));
|
|
7472
|
+
let inter = 0;
|
|
7473
|
+
for (const t of tokens) if (etoks.has(t)) inter++;
|
|
7474
|
+
const coverage = inter / tokens.size;
|
|
7475
|
+
if (coverage > bestCoverage) { bestCoverage = coverage; best = e; }
|
|
7476
|
+
}
|
|
7477
|
+
return { entry: bestCoverage >= minCoverage ? best : null, coverage: bestCoverage, best: best };
|
|
7478
|
+
}
|
|
7479
|
+
function contextAuditRememberCommand(text) {
|
|
7480
|
+
const clean = String(text || '').replace(/"/g, "'").trim();
|
|
7481
|
+
const specificity = clean.replace(/^[^::]*[::]\s*/, '').trim();
|
|
7482
|
+
const subject = contextAuditSuggestionSubject(specificity || clean, 20);
|
|
7483
|
+
return 'yotta-memory remember FACT "' + subject + '" "' + clean + '" --verify';
|
|
7484
|
+
}
|
|
7485
|
+
function contextAuditSuggestionSubject(text, max) {
|
|
7486
|
+
// v0.18.0 修正:截断回退到非标点边界,避免切在「(」这类标点中间。
|
|
7487
|
+
const limit = parseInt(max, 10) || 20;
|
|
7488
|
+
const clean = String(text || '').trim();
|
|
7489
|
+
if (clean.length <= limit) return clean;
|
|
7490
|
+
const cut = clean.slice(0, limit);
|
|
7491
|
+
const trimmed = cut.replace(/[\s,。;:、,.;:!?!?()()【】\[\]「」『』“”'"—–\-]+$/, '');
|
|
7492
|
+
return trimmed.length >= 8 ? trimmed : cut;
|
|
7493
|
+
}
|
|
7494
|
+
function contextAuditCore(opts) {
|
|
7495
|
+
opts = opts || {};
|
|
7496
|
+
const roots = memoryRoots();
|
|
7497
|
+
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
7498
|
+
const ident = resolveIdentity(opts);
|
|
7499
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
7500
|
+
if (!ident.id) {
|
|
7501
|
+
return {
|
|
7502
|
+
error: true,
|
|
7503
|
+
exitCode: 3,
|
|
7504
|
+
text: 'context --audit 需要先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
7505
|
+
};
|
|
7506
|
+
}
|
|
7507
|
+
const selfAgent = ident.id;
|
|
7508
|
+
const owner = opts.owner || selfAgent;
|
|
7509
|
+
const root = userRoot();
|
|
7510
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
7511
|
+
if (keyError) return { error: true, exitCode: 3, text: keyError };
|
|
7512
|
+
const cfg = loadConfig();
|
|
7513
|
+
const rawMin = parseFloat(cfg.context_audit_min_coverage);
|
|
7514
|
+
const minCoverage = (rawMin > 0 && rawMin <= 1) ? rawMin : 0.5;
|
|
7515
|
+
let input = null;
|
|
7516
|
+
let source = '';
|
|
7517
|
+
if (opts.text !== undefined && opts.text !== null && String(opts.text) !== '') {
|
|
7518
|
+
// v0.18.0:内联文本输入(MCP 侧用它替代文件路径,避免把 --from 变成任意文件读取通道)。
|
|
7519
|
+
input = String(opts.text);
|
|
7520
|
+
source = 'inline';
|
|
7521
|
+
} else if (opts.from !== undefined && opts.from !== null && String(opts.from) !== '') {
|
|
7522
|
+
if (String(opts.from) === '-') {
|
|
7523
|
+
input = fs.readFileSync(0, 'utf8');
|
|
7524
|
+
source = 'stdin';
|
|
7525
|
+
} else {
|
|
7526
|
+
const fp = path.resolve(String(opts.from));
|
|
7527
|
+
if (!fs.existsSync(fp)) return { error: true, exitCode: 2, text: '输入文件不存在: ' + fp };
|
|
7528
|
+
input = fs.readFileSync(fp, 'utf8');
|
|
7529
|
+
source = fp;
|
|
7530
|
+
}
|
|
7531
|
+
} else {
|
|
7532
|
+
// noStdin:MCP(stdio)的 fd 0 是协议流,绝不能被当成待审计内容读走。
|
|
7533
|
+
const piped = opts.noStdin ? null : contextAuditReadStdin();
|
|
7534
|
+
if (piped !== null && piped.trim()) { input = piped; source = 'stdin'; }
|
|
7535
|
+
}
|
|
7536
|
+
const entries = [];
|
|
7537
|
+
for (const r of roots) {
|
|
7538
|
+
for (const e of indexEntriesFor(r, {}).entries) {
|
|
7539
|
+
if (classifyRead(e, owner, '', !!opts.unsafe, selfAgent) === 'denied') continue;
|
|
7540
|
+
entries.push(e);
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
const gate = contextAuditGate(opts);
|
|
7544
|
+
if (input === null) {
|
|
7545
|
+
const ctx = contextCore(Object.assign({}, opts, { audit: false, explain: true, noUsage: true }));
|
|
7546
|
+
const drops = (ctx.trace || []).filter(function (t) { return t.indexOf('[dropped]') === 0; });
|
|
7547
|
+
const items = drops.map(function (line) {
|
|
7548
|
+
const m = /^\[dropped\]\s+(\S+)\s+reason:\s+(\S+)/.exec(line);
|
|
7549
|
+
const file = m ? m[1] : '';
|
|
7550
|
+
const reason = m ? m[2] : '';
|
|
7551
|
+
const found = entries.filter(function (e) { return e.file === file; })[0];
|
|
7552
|
+
return {
|
|
7553
|
+
file: file,
|
|
7554
|
+
reason: reason,
|
|
7555
|
+
recallable: !!found,
|
|
7556
|
+
subject: found ? found.subject : '',
|
|
7557
|
+
};
|
|
7558
|
+
});
|
|
7559
|
+
const unrecallable = items.filter(function (x) { return !x.recallable; }).length;
|
|
7560
|
+
const lines = ['## yotta-memory context --audit(丢弃清单审计)', ''];
|
|
7561
|
+
lines.push('- 模式: 无输入内容,审计当前上下文包的丢弃清单(duplicate / budget_exceeded)');
|
|
7562
|
+
lines.push('- 计数: dropped ' + items.length + ' 条 / 仍可召回 ' + (items.length - unrecallable) + ' 条 / 无法召回 ' + unrecallable + ' 条');
|
|
7563
|
+
lines.push('');
|
|
7564
|
+
if (!items.length) lines.push('(本次上下文包无丢弃项。)');
|
|
7565
|
+
for (const item of items) {
|
|
7566
|
+
lines.push('- ' + item.file + '(' + item.reason + ')' + (item.recallable
|
|
7567
|
+
? ' → 仍可召回: yotta-memory recall "' + String(item.subject).replace(/"/g, "'") + '"'
|
|
7568
|
+
: ' → 未在当前可读索引中找到,请人工核对'));
|
|
7569
|
+
}
|
|
7570
|
+
if (gate !== null) lines.push('', '- --gate ' + gate + ': 无法召回条数 ' + unrecallable + '(超过门槛时 exit 1)');
|
|
7571
|
+
return {
|
|
7572
|
+
error: false,
|
|
7573
|
+
exitCode: (gate !== null && unrecallable > gate) ? 1 : 0,
|
|
7574
|
+
text: lines.join('\n'),
|
|
7575
|
+
report: {
|
|
7576
|
+
schemaVersion: 1,
|
|
7577
|
+
mode: 'dropped',
|
|
7578
|
+
summary: { dropped: items.length, recallable: items.length - unrecallable, unrecallable: unrecallable },
|
|
7579
|
+
items: items,
|
|
7580
|
+
},
|
|
7581
|
+
};
|
|
7582
|
+
}
|
|
7583
|
+
const segments = contextAuditSegments(input);
|
|
7584
|
+
const items = segments.map(function (text) {
|
|
7585
|
+
const cov = contextAuditCoverage(text, entries, minCoverage);
|
|
7586
|
+
let status = 'missing';
|
|
7587
|
+
let matched = null;
|
|
7588
|
+
if (cov.entry) { status = 'landed'; matched = cov.entry; }
|
|
7589
|
+
else if (cov.best && cov.coverage > 0 && cov.coverage >= minCoverage * 0.6) status = 'uncertain';
|
|
7590
|
+
return {
|
|
7591
|
+
text: text,
|
|
7592
|
+
status: status,
|
|
7593
|
+
coverage: round3(cov.coverage),
|
|
7594
|
+
matched_file: matched ? matched.file : '',
|
|
7595
|
+
matched_subject: matched ? matched.subject : '',
|
|
7596
|
+
suggested_command: status === 'missing' ? contextAuditRememberCommand(text) : '',
|
|
7597
|
+
};
|
|
7598
|
+
});
|
|
7599
|
+
const landed = items.filter(function (x) { return x.status === 'landed'; });
|
|
7600
|
+
const missing = items.filter(function (x) { return x.status === 'missing'; });
|
|
7601
|
+
const uncertain = items.filter(function (x) { return x.status === 'uncertain'; });
|
|
7602
|
+
const lines = ['## yotta-memory context --audit(上下文压缩审计)', ''];
|
|
7603
|
+
lines.push('- 来源: ' + source + '(只读;不会自动补写记忆)');
|
|
7604
|
+
lines.push('- 汇总: 候选 ' + items.length + ' 条 / 已落盘 ' + landed.length + ' 条 / 未落盘 ' + missing.length + ' 条 / 无法判定 ' + uncertain.length + ' 条');
|
|
7605
|
+
lines.push('');
|
|
7606
|
+
lines.push('### 未落盘(建议落盘,命令仅打印不执行)');
|
|
7607
|
+
if (!missing.length) lines.push('(无)');
|
|
7608
|
+
for (const item of missing) {
|
|
7609
|
+
lines.push('- ' + item.text.slice(0, 120));
|
|
7610
|
+
lines.push(' → ' + item.suggested_command);
|
|
7611
|
+
}
|
|
7612
|
+
lines.push('');
|
|
7613
|
+
lines.push('### 无法判定(人工核对)');
|
|
7614
|
+
if (!uncertain.length) lines.push('(无)');
|
|
7615
|
+
for (const item of uncertain) lines.push('- ' + item.text.slice(0, 120) + '(覆盖率 ' + item.coverage + ')');
|
|
7616
|
+
lines.push('');
|
|
7617
|
+
lines.push('### 已落盘');
|
|
7618
|
+
if (!landed.length) lines.push('(无)');
|
|
7619
|
+
for (const item of landed) lines.push('- ' + item.text.slice(0, 120) + ' → ' + item.matched_file);
|
|
7620
|
+
if (gate !== null) lines.push('', '- --gate ' + gate + ': 未落盘 ' + missing.length + ' 条(超过门槛时 exit 1)');
|
|
7621
|
+
return {
|
|
7622
|
+
error: false,
|
|
7623
|
+
exitCode: (gate !== null && missing.length > gate) ? 1 : 0,
|
|
7624
|
+
text: lines.join('\n'),
|
|
7625
|
+
report: {
|
|
7626
|
+
schemaVersion: 1,
|
|
7627
|
+
mode: 'input',
|
|
7628
|
+
source: source,
|
|
7629
|
+
thresholds: { min_coverage: minCoverage },
|
|
7630
|
+
summary: { candidates: items.length, landed: landed.length, missing: missing.length, uncertain: uncertain.length },
|
|
7631
|
+
items: items,
|
|
7632
|
+
},
|
|
7633
|
+
};
|
|
7634
|
+
}
|
|
7635
|
+
|
|
6846
7636
|
// ---- v0.17.0 A1: bench 可复算基准评测(只读 / 本地 / 零依赖)----
|
|
6847
7637
|
const BENCH_SCHEMA_VERSION = 1;
|
|
6848
7638
|
const BENCH_DEFAULT_SEED = 20260925;
|
|
@@ -7931,17 +8721,18 @@ async function cmdScan(opts) {
|
|
|
7931
8721
|
|
|
7932
8722
|
// ---- config 命令 ----
|
|
7933
8723
|
// v0.10.0:maintain_* / consolidate_* 数值键纳入 config set/get(此前只支持 3 个键但文档已写可调)
|
|
7934
|
-
function isNumericConfigKey(key) { return /^(maintain_|consolidate_|scale_|backup_max_age_hours$)/.test(key); }
|
|
8724
|
+
function isNumericConfigKey(key) { return /^(maintain_|consolidate_|scale_|backup_max_age_hours$|usage_retention_days$|usage_query_slots$|capacity_|promotion_|context_audit_min_coverage$)/.test(key); }
|
|
7935
8725
|
// v0.17.0 B2:doctor 规模体检阈值键(0 表示只要超过 0 就告警,便于压测与演练)
|
|
7936
|
-
const SCALE_CONFIG_KEYS = ['scale_warn_entries', 'scale_warn_files_per_dir', 'scale_warn_index_bytes', 'scale_warn_cold_start_ms'];
|
|
8726
|
+
const SCALE_CONFIG_KEYS = ['scale_warn_entries', 'scale_warn_files_per_dir', 'scale_warn_index_bytes', 'scale_warn_cold_start_ms', 'scale_info_entries', 'scale_info_files_per_dir', 'scale_info_index_bytes', 'scale_info_cold_start_ms'];
|
|
7937
8727
|
function cmdConfigSet(key, value) {
|
|
7938
|
-
const known = ['memory_home', 'embedding_cmd', 'embedding_timeout', 'backup_dir', 'backup_enabled', 'backup_schedule', 'backup_time', 'backup_max_age_hours', 'backup_setup_choice', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period'].concat(SCALE_CONFIG_KEYS);
|
|
7939
|
-
if (known.indexOf(key) === -1) { console.error('未知配置项: ' + key + '(可用: memory_home / backup_dir / embedding_cmd / embedding_timeout / maintain_* / consolidate_* / scale_*)'); process.exit(2); }
|
|
8728
|
+
const known = ['memory_home', 'embedding_cmd', 'embedding_timeout', 'backup_dir', 'backup_enabled', 'backup_schedule', 'backup_time', 'backup_max_age_hours', 'backup_setup_choice', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period', 'usage_enabled', 'usage_retention_days', 'usage_query_slots', 'capacity_cooldown_days', 'capacity_warn_bytes', 'capacity_candidate_limit', 'promotion_min_hits', 'promotion_min_queries', 'context_audit_min_coverage'].concat(SCALE_CONFIG_KEYS);
|
|
8729
|
+
if (known.indexOf(key) === -1) { console.error('未知配置项: ' + key + '(可用: memory_home / backup_dir / embedding_cmd / embedding_timeout / maintain_* / consolidate_* / scale_* / usage_* / capacity_* / promotion_*)'); process.exit(2); }
|
|
7940
8730
|
if (value === undefined || value === null || value === '') { console.error('缺少值: config set ' + key + ' <值>'); process.exit(2); }
|
|
7941
8731
|
const cfg = loadConfig();
|
|
7942
8732
|
if (key === 'memory_home') cfg.memory_home = value;
|
|
7943
8733
|
else if (key === 'backup_dir') cfg.backup_dir = value;
|
|
7944
8734
|
else if (key === 'backup_enabled') cfg.backup_enabled = value !== 'false' && value !== '0';
|
|
8735
|
+
else if (key === 'usage_enabled') cfg.usage_enabled = value !== 'false' && value !== '0';
|
|
7945
8736
|
else if (key === 'backup_schedule' || key === 'backup_time' || key === 'backup_setup_choice') cfg[key] = value;
|
|
7946
8737
|
else if (key === 'embedding_cmd') cfg.embedding_cmd = value;
|
|
7947
8738
|
else if (key === 'embedding_timeout') cfg.embedding_timeout = parseInt(value, 10) || 3000;
|
|
@@ -7967,11 +8758,12 @@ function cmdConfigGet(opts) {
|
|
|
7967
8758
|
console.log('backup_enabled: ' + (cfg.backup_enabled === undefined ? '(未设置)' : cfg.backup_enabled));
|
|
7968
8759
|
console.log('backup_schedule: ' + (cfg.backup_schedule || '(未设置)'));
|
|
7969
8760
|
console.log('backup_time: ' + (cfg.backup_time || '(未设置)'));
|
|
8761
|
+
console.log('usage_enabled: ' + (cfg.usage_enabled === undefined ? '(默认 true)' : cfg.usage_enabled));
|
|
7970
8762
|
console.log('embedding_cmd: ' + (cfg.embedding_cmd || '(未设置)'));
|
|
7971
8763
|
console.log('embedding_timeout: ' + (cfg.embedding_timeout || 3000));
|
|
7972
|
-
const keys = ['memory_home', 'backup_dir', 'backup_enabled', 'backup_schedule', 'backup_time', 'backup_max_age_hours', 'backup_setup_choice', 'embedding_cmd', 'embedding_timeout', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period'].concat(SCALE_CONFIG_KEYS);
|
|
8764
|
+
const keys = ['memory_home', 'backup_dir', 'backup_enabled', 'backup_schedule', 'backup_time', 'backup_max_age_hours', 'backup_setup_choice', 'embedding_cmd', 'embedding_timeout', 'maintain_archived_utility', 'maintain_archived_age', 'maintain_forget_utility', 'maintain_forget_age', 'maintain_decay_halflife_FACT', 'maintain_decay_halflife_PREF', 'maintain_decay_halflife_COMMIT', 'consolidate_min_age', 'consolidate_min_idle', 'consolidate_max_utility', 'consolidate_min_group', 'consolidate_period', 'usage_enabled', 'usage_retention_days', 'usage_query_slots', 'capacity_cooldown_days', 'capacity_warn_bytes', 'capacity_candidate_limit', 'promotion_min_hits', 'promotion_min_queries', 'context_audit_min_coverage'].concat(SCALE_CONFIG_KEYS);
|
|
7973
8765
|
for (const k of keys) {
|
|
7974
|
-
if (k === 'memory_home' || k === 'backup_dir' || k === 'backup_enabled' || k === 'backup_schedule' || k === 'backup_time' || k === 'embedding_cmd' || k === 'embedding_timeout') continue;
|
|
8766
|
+
if (k === 'memory_home' || k === 'backup_dir' || k === 'backup_enabled' || k === 'backup_schedule' || k === 'backup_time' || k === 'embedding_cmd' || k === 'embedding_timeout' || k === 'usage_enabled') continue;
|
|
7975
8767
|
if (cfg[k] !== undefined) console.log(k + ': ' + cfg[k]);
|
|
7976
8768
|
}
|
|
7977
8769
|
console.log('当前生效用户级位置: ' + userRoot());
|
|
@@ -7988,17 +8780,18 @@ function mcpTools(profile) {
|
|
|
7988
8780
|
{ name: 'remember', description: '写入一条记忆。参数 type(FACT/PREF/BOUND/COMMIT)、subject、statement 必填;owner 可选,默认当前智能体', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'FACT/PREF/BOUND/COMMIT' }, subject: { type: 'string' }, statement: { type: 'string' }, owner: { type: 'string' } }, required: ['type', 'subject', 'statement'] } },
|
|
7989
8781
|
{ name: 'recall', description: '检索记忆。query 可选;type 可选;limit 可选(默认 20);explain 可选。只返回当前智能体可读记忆;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
|
|
7990
8782
|
{ name: 'search', description: '检索记忆(同 recall)。query 可选;type 可选;limit 可选(默认 20);explain 可选;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
|
|
7991
|
-
{ name: 'context', description: '生成开工上下文包。focus 可选;limit 可选;budget 可选;explain 可选;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { focus: { type: 'string' }, limit: { type: 'number' }, budget: { type: 'number' }, explain: { type: 'boolean' }, embeddingTimeout: { type: 'number' } } } },
|
|
8783
|
+
{ name: 'context', description: '生成开工上下文包。focus 可选;limit 可选;budget 可选;explain 可选;audit=true 时改为上下文压缩审计(用 auditText 传待审计文本,只读),gate 控制未落盘条数门槛;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { focus: { type: 'string' }, limit: { type: 'number' }, budget: { type: 'number' }, explain: { type: 'boolean' }, embeddingTimeout: { type: 'number' }, audit: { type: 'boolean' }, auditText: { type: 'string' }, gate: { type: 'number' } } } },
|
|
7992
8784
|
{ name: 'doctor', description: '开工可靠性检查:检查记忆库根目录、密钥库、索引、身份登记与最近备份;只读,不会修改记忆', inputSchema: { type: 'object', properties: {} } },
|
|
7993
8785
|
{ name: 'forget', description: '删除一条记忆。file 为记忆文件路径(如 facts/2026-08-24-0001.md 或文件名)', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
7994
|
-
{ name: 'archive', description: '归档旧记忆。days 默认 180;threshold 默认 0.4', inputSchema: { type: 'object', properties: { days: { type: 'number' }, threshold: { type: 'number' } } } },
|
|
8786
|
+
{ name: 'archive', description: '归档旧记忆。days 默认 180;threshold 默认 0.4;dryRun=true 只预览、不落盘。破坏性覆盖 --force 仅本机 CLI 提供(MCP 不接受)', inputSchema: { type: 'object', properties: { days: { type: 'number' }, threshold: { type: 'number' }, dryRun: { type: 'boolean' } } } },
|
|
7995
8787
|
{ name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
|
|
7996
8788
|
{ name: 'export', description: '导出全部记忆到记忆库内的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json;仅限记忆库内路径)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
|
|
7997
8789
|
{ name: 'import', description: '从记忆库内的 JSON 文件导入记忆。src 为文件路径(相对记忆库目录,或记忆库内绝对路径;仅限记忆库内)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
|
|
7998
8790
|
{ name: 'agent_info', description: '查看当前智能体身份与登记状态(HTTP 读经 token 校验的 X-Agent-Id;stdio 读 --agent-id 显式参数)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
|
|
7999
8791
|
{ name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
|
|
8000
8792
|
{ name: 'feedback', description: '显式使用反馈(自我学习):useful/useless 调整记忆 weight/confidence/feedback_net。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' }, useful: { type: 'boolean' }, useless: { type: 'boolean' }, reason: { type: 'string' } }, required: ['file'] } },
|
|
8001
|
-
{ name: 'maintain', description: '记忆自组织(自我进化):规则层归档/遗忘/去重预览。默认 dry-run;apply 才执行,purge
|
|
8793
|
+
{ name: 'maintain', description: '记忆自组织(自我进化):规则层归档/遗忘/去重预览。默认 dry-run;apply 才执行,purge 才真删;capacity=true 改为只读容量水位报告', inputSchema: { type: 'object', properties: { apply: { type: 'boolean' }, purge: { type: 'boolean' }, dedup: { type: 'boolean' }, capacity: { type: 'boolean' }, limit: { type: 'number' }, threshold: { type: 'number' }, age: { type: 'number' } } } },
|
|
8794
|
+
{ name: 'consolidate', description: '周期摘要压缩候选报告(只读 propose 模式)。MCP 不提供 --apply / --undo / --batches;执行与回滚请在本机 CLI 由用户确认后运行', inputSchema: { type: 'object', properties: { minAge: { type: 'number' }, minIdle: { type: 'number' }, maxUtility: { type: 'number' }, minGroup: { type: 'number' }, period: { type: 'number' }, type: { type: 'string' } } } },
|
|
8002
8795
|
{ name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体(MCP 不支持 --model 外部命令)', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' } } } },
|
|
8003
8796
|
{ name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
8004
8797
|
];
|
|
@@ -8040,6 +8833,16 @@ function callToolInner(name, args, ctx) {
|
|
|
8040
8833
|
return { text: r.text, error: r.error };
|
|
8041
8834
|
}
|
|
8042
8835
|
if (name === 'context') {
|
|
8836
|
+
if (args.audit) {
|
|
8837
|
+
// v0.18.0 A7:MCP 侧只接受内联文本(不接受文件路径,避免把 --from 变成任意文件读取通道),
|
|
8838
|
+
// 也不读 stdin(stdio MCP 的 fd 0 是协议流)。
|
|
8839
|
+
const auditText = (args.auditText !== undefined && args.auditText !== null) ? String(args.auditText) : '';
|
|
8840
|
+
if (!auditText.trim()) {
|
|
8841
|
+
return { text: '拒绝: MCP 的 context audit 只接受内联文本(auditText)。审计本地文件或标准输入请在本机 CLI 运行 yotta-memory context --audit --from <文件|->。', error: true };
|
|
8842
|
+
}
|
|
8843
|
+
const r = contextAuditCore({ selfAgent: agent, owner: agent, text: auditText, gate: args.gate, noStdin: true });
|
|
8844
|
+
return { text: r.text, error: r.error };
|
|
8845
|
+
}
|
|
8043
8846
|
const r = contextCore({
|
|
8044
8847
|
focus: args.focus ? String(args.focus) : '',
|
|
8045
8848
|
limit: args.limit || 10,
|
|
@@ -8059,7 +8862,7 @@ function callToolInner(name, args, ctx) {
|
|
|
8059
8862
|
return { text: r.text, error: r.error };
|
|
8060
8863
|
}
|
|
8061
8864
|
if (name === 'archive') {
|
|
8062
|
-
const r = archiveCore({ selfAgent: agent, days: args.days, threshold: args.threshold });
|
|
8865
|
+
const r = archiveCore({ selfAgent: agent, days: args.days, threshold: args.threshold, dryRun: !!args.dryRun });
|
|
8063
8866
|
return { text: r.text, error: r.error };
|
|
8064
8867
|
}
|
|
8065
8868
|
if (name === 'reindex') {
|
|
@@ -8116,7 +8919,17 @@ function callToolInner(name, args, ctx) {
|
|
|
8116
8919
|
return { text: r.text, error: r.error };
|
|
8117
8920
|
}
|
|
8118
8921
|
if (name === 'maintain') {
|
|
8119
|
-
const r =
|
|
8922
|
+
const r = args.capacity
|
|
8923
|
+
? capacityCore({ selfAgent: agent, limit: args.limit })
|
|
8924
|
+
: maintainCore({ selfAgent: agent, apply: !!args.apply, purge: !!args.purge, dedup: !!args.dedup, threshold: args.threshold, age: args.age });
|
|
8925
|
+
return { text: r.text, error: r.error };
|
|
8926
|
+
}
|
|
8927
|
+
if (name === 'consolidate') {
|
|
8928
|
+
// v0.18.0 A5:MCP 只读 propose;apply / undo / batches 留在本机 CLI(由用户确认后执行)。
|
|
8929
|
+
if (args.apply || args.undo || args.batches || args.yes) {
|
|
8930
|
+
return { text: '拒绝: MCP 的 consolidate 只提供只读 propose 报告;--apply / --undo / --batches 请在本机 CLI 由用户确认后运行(AI 不得代替用户执行 --apply)。', error: true };
|
|
8931
|
+
}
|
|
8932
|
+
const r = consolidateCore({ selfAgent: agent, apply: false, minAge: args.minAge, minIdle: args.minIdle, maxUtility: args.maxUtility, minGroup: args.minGroup, period: args.period, type: args.type });
|
|
8120
8933
|
return { text: r.text, error: r.error };
|
|
8121
8934
|
}
|
|
8122
8935
|
if (name === 'distill') {
|
|
@@ -9058,6 +9871,37 @@ function applyConsolidateGroup(root, group, batch, period) {
|
|
|
9058
9871
|
return { made: 1, moved: moved, summaryRel: summaryRel };
|
|
9059
9872
|
}
|
|
9060
9873
|
|
|
9874
|
+
// v0.18.0 A5:首次启用说明按 .archive 审计判定(不新建标记文件)。
|
|
9875
|
+
function consolidateHasManifest(root) {
|
|
9876
|
+
const dir = path.join(root, ARCHIVE_DIR);
|
|
9877
|
+
let names;
|
|
9878
|
+
try { names = fs.readdirSync(dir); } catch (e) { return false; }
|
|
9879
|
+
for (const name of names) {
|
|
9880
|
+
if (!/^audit-\d{4}-\d{2}-\d{2}\.jsonl$/.test(name)) continue;
|
|
9881
|
+
let raw;
|
|
9882
|
+
try { raw = fs.readFileSync(path.join(dir, name), 'utf8'); } catch (e) { continue; }
|
|
9883
|
+
for (const line of raw.split('\n')) {
|
|
9884
|
+
if (!line.trim()) continue;
|
|
9885
|
+
let rec;
|
|
9886
|
+
try { rec = JSON.parse(line); } catch (e) { continue; }
|
|
9887
|
+
if (rec && rec.action === 'manifest' && rec.command === 'consolidate') return true;
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
return false;
|
|
9891
|
+
}
|
|
9892
|
+
function consolidateFirstRunNotice() {
|
|
9893
|
+
return [
|
|
9894
|
+
'### 首次启用说明(只显示一次)',
|
|
9895
|
+
'- 保留期:原文移入 .archive/ 后一直保留,直到你显式 purge 或 undo;不会自动删除。',
|
|
9896
|
+
'- 检查:yotta-memory consolidate(默认 --propose,只出报告不写盘)。',
|
|
9897
|
+
'- 纠正:yotta-memory consolidate --undo <batch>。',
|
|
9898
|
+
'- 导出:yotta-memory export。',
|
|
9899
|
+
'- 停用:不再执行 --apply,或用 yotta-memory config set consolidate_min_age 99999 抬高门槛。',
|
|
9900
|
+
'- 删除:yotta-memory maintain --purge(对遗忘候选真删)。',
|
|
9901
|
+
'- AI 不得代替用户执行 --apply;--apply 需要确认,非交互必须显式加 --yes。',
|
|
9902
|
+
];
|
|
9903
|
+
}
|
|
9904
|
+
|
|
9061
9905
|
function consolidateCore(opts) {
|
|
9062
9906
|
opts = opts || {};
|
|
9063
9907
|
if (opts.undo) return consolidateUndoCore(String(opts.undo), opts);
|
|
@@ -9065,6 +9909,15 @@ function consolidateCore(opts) {
|
|
|
9065
9909
|
const root = userRoot();
|
|
9066
9910
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
9067
9911
|
const apply = !!opts.apply;
|
|
9912
|
+
// v0.18.0 A5:--apply 是破坏性写入;非交互必须显式 --yes,交互确认在 cmdConsolidate 完成。
|
|
9913
|
+
if (apply && !opts.yes) {
|
|
9914
|
+
return {
|
|
9915
|
+
error: true,
|
|
9916
|
+
exitCode: 2,
|
|
9917
|
+
text: '拒绝: consolidate --apply 会移动原文并生成摘要(原文保留在 .archive/,可用 --undo 回滚)。请先运行 yotta-memory consolidate 审阅候选报告;交互式执行会要求输入确认串,非交互必须显式加 --yes。',
|
|
9918
|
+
};
|
|
9919
|
+
}
|
|
9920
|
+
const firstRunNotice = consolidateHasManifest(root) ? [] : consolidateFirstRunNotice();
|
|
9068
9921
|
const selfAgent = resolveIdentity(opts).id;
|
|
9069
9922
|
const unsafe = !!opts.unsafe;
|
|
9070
9923
|
const cfg = loadConfig();
|
|
@@ -9076,7 +9929,7 @@ function consolidateCore(opts) {
|
|
|
9076
9929
|
const period = (opts.period !== undefined && opts.period !== null) ? opts.period : (parseInt(cfg.consolidate_period || '90', 10) || 90);
|
|
9077
9930
|
const typeOnly = opts.type ? String(opts.type).toUpperCase() : '';
|
|
9078
9931
|
const lines = ['## yotta-memory consolidate(周期摘要压缩)'];
|
|
9079
|
-
lines.push('- 模式: ' + (apply ? '
|
|
9932
|
+
lines.push('- 模式: ' + (apply ? 'apply(执行压缩)' : 'propose(只出报告,未改动;确认后加 --apply --yes 执行)'));
|
|
9080
9933
|
lines.push('- 候选: 天龄 ≥ ' + minAge + ' 天 ∧ 闲置 ≥ ' + minIdle + ' 天 ∧ utility ≤ ' + maxU + (typeOnly ? ' ∧ 仅类型 ' + typeOnly : '') + ';immutable / BOUND 豁免');
|
|
9081
9934
|
lines.push('');
|
|
9082
9935
|
const cands = [];
|
|
@@ -9099,16 +9952,59 @@ function consolidateCore(opts) {
|
|
|
9099
9952
|
if (age < minAge || idle < minIdle || u > maxU) { stats.active++; continue; }
|
|
9100
9953
|
cands.push({ e: e, owner: owner, type: e.type, age: age, idle: idle, utility: u, scope: e.scope === 'private' ? 'private' : 'public' });
|
|
9101
9954
|
}
|
|
9955
|
+
const makeConsolidateGroup = function (g) {
|
|
9956
|
+
const type = g[0].type;
|
|
9957
|
+
const owner = g[0].scope === 'private' ? (g[0].owner || '') : '';
|
|
9958
|
+
const ages = g.map(function (c) { return c.age; });
|
|
9959
|
+
const theme = pickSummaryTheme(g);
|
|
9960
|
+
return {
|
|
9961
|
+
type: type,
|
|
9962
|
+
scope: g[0].scope === 'private' ? 'private' : 'public',
|
|
9963
|
+
owner: owner,
|
|
9964
|
+
count: g.length,
|
|
9965
|
+
age_min: Math.min.apply(null, ages),
|
|
9966
|
+
age_max: Math.max.apply(null, ages),
|
|
9967
|
+
avg_utility: round3(g.reduce(function (s, c) { return s + c.utility; }, 0) / g.length),
|
|
9968
|
+
theme: theme,
|
|
9969
|
+
summary_subject: '周期摘要 ' + theme + '(' + period + ' 天窗)',
|
|
9970
|
+
files: g.map(function (c) { return c.e.file; }),
|
|
9971
|
+
archive_to: path.relative(root, archiveDirFor(root, type, owner)).replace(/\\/g, '/'),
|
|
9972
|
+
};
|
|
9973
|
+
};
|
|
9974
|
+
const makeConsolidateReport = function (builtGroups, batch) {
|
|
9975
|
+
return {
|
|
9976
|
+
schemaVersion: 1,
|
|
9977
|
+
mode: apply ? 'apply' : 'propose',
|
|
9978
|
+
batch: batch || '',
|
|
9979
|
+
root: root,
|
|
9980
|
+
thresholds: {
|
|
9981
|
+
min_age: minAge,
|
|
9982
|
+
min_idle: minIdle,
|
|
9983
|
+
max_utility: maxU,
|
|
9984
|
+
min_group: minGroup,
|
|
9985
|
+
period: period,
|
|
9986
|
+
},
|
|
9987
|
+
stats: stats,
|
|
9988
|
+
candidates: cands.length,
|
|
9989
|
+
groups: builtGroups.map(makeConsolidateGroup),
|
|
9990
|
+
retention: '原文移入 .archive/ 后一直保留,直到显式 purge 或 undo。',
|
|
9991
|
+
rollback: batch
|
|
9992
|
+
? ('yotta-memory consolidate --undo ' + batch)
|
|
9993
|
+
: 'yotta-memory consolidate --undo <batch>(执行后批次号见报告)',
|
|
9994
|
+
};
|
|
9995
|
+
};
|
|
9102
9996
|
if (!cands.length) {
|
|
9103
9997
|
lines.push('(无可压缩候选;扫描 ' + stats.scan + ' 条,跳过: 其它 owner/无密钥 ' + stats.denied + ' / immutable ' + stats.immutable + ' / BOUND ' + stats.bound + ' / 类型过滤 ' + stats.typeSkip + ' / 无 created ' + stats.noCreated + ' / 不够老或仍活跃 ' + stats.active + ')');
|
|
9104
|
-
|
|
9998
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
9999
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport([], '') };
|
|
9105
10000
|
}
|
|
9106
10001
|
const groups = clusterConsolidateCandidates(cands);
|
|
9107
10002
|
lines.push('候选 ' + cands.length + ' 条(扫描 ' + stats.scan + ',跳过: 其它 owner/无密钥 ' + stats.denied + ' / immutable ' + stats.immutable + ' / BOUND ' + stats.bound + ' / 类型过滤 ' + stats.typeSkip + ' / 无 created ' + stats.noCreated + ' / 不够老或仍活跃 ' + stats.active + ')');
|
|
9108
10003
|
if (!groups.length) {
|
|
9109
10004
|
lines.push('');
|
|
9110
10005
|
lines.push('(无可归纳组:同主题聚类 ≥ ' + minGroup + ' 条才生成周期摘要;零散单条旧记忆请用 maintain --apply 归档)');
|
|
9111
|
-
|
|
10006
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
10007
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport([], '') };
|
|
9112
10008
|
}
|
|
9113
10009
|
lines.push('');
|
|
9114
10010
|
lines.push('### 周期摘要组(' + groups.length + ' 组,每组将生成摘要 1 条 + 归档原文 N 条)');
|
|
@@ -9119,12 +10015,16 @@ function consolidateCore(opts) {
|
|
|
9119
10015
|
const minA = Math.min.apply(null, ages), maxA = Math.max.apply(null, ages);
|
|
9120
10016
|
const avgU = round3(g.reduce(function (s, c) { return s + c.utility; }, 0) / g.length);
|
|
9121
10017
|
lines.push('- [' + type + '] 主题: ' + pickSummaryTheme(g) + '(' + scopeLabel + ',' + g.length + ' 条,' + minA + '~' + maxA + ' 天,平均效用 ' + avgU + ')' + (type === 'COMMIT' ? ' ⚠️ COMMIT 承诺/任务类,请确认' : ''));
|
|
10018
|
+
lines.push(' → 将生成 周期摘要 ' + pickSummaryTheme(g) + '(' + period + ' 天窗),原文归档到 ' + path.relative(root, archiveDirFor(root, type, g[0].owner || '')).replace(/\\/g, '/'));
|
|
9122
10019
|
for (const c of g) lines.push(' · ' + c.e.file + ' — ' + String(c.e.subject || '').slice(0, 32) + ': ' + String(c.e.statement || '').slice(0, 60));
|
|
9123
10020
|
}
|
|
9124
10021
|
if (!apply) {
|
|
9125
10022
|
lines.push('');
|
|
9126
|
-
lines.push('
|
|
9127
|
-
|
|
10023
|
+
lines.push('- 保留期: 原文移入 .archive/ 后一直保留,直到显式 purge 或 undo。');
|
|
10024
|
+
lines.push('- 回滚: 执行后可用 yotta-memory consolidate --undo <batch> 恢复。');
|
|
10025
|
+
lines.push('未执行任何变更。确认后加 --apply(非交互再加 --yes)执行。');
|
|
10026
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
10027
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport(groups, '') };
|
|
9128
10028
|
}
|
|
9129
10029
|
const guard = destructiveGuardCore({
|
|
9130
10030
|
root: root,
|
|
@@ -9149,12 +10049,35 @@ function consolidateCore(opts) {
|
|
|
9149
10049
|
}
|
|
9150
10050
|
appendAudit(root, 'audit', { action: 'batch_done', ts: new Date().toISOString(), batch: batch, command: 'consolidate', summaries: made, archived: moved, active_after: collectEntryFiles(root).length });
|
|
9151
10051
|
lines.push('- 完成: 生成摘要 ' + made + ' 条 / 原文归档 ' + moved + ' 条');
|
|
9152
|
-
|
|
10052
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
10053
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport(groups, batch) };
|
|
9153
10054
|
}
|
|
9154
10055
|
|
|
9155
|
-
function cmdConsolidate(opts) {
|
|
10056
|
+
async function cmdConsolidate(opts) {
|
|
10057
|
+
// v0.18.0 A5:交互式确认串(X 组 / Y 条);非交互明确拒绝并提示 --yes。
|
|
10058
|
+
if (opts.apply && !opts.yes) {
|
|
10059
|
+
if (!process.stdin.isTTY) {
|
|
10060
|
+
const blocked = consolidateCore(opts);
|
|
10061
|
+
console.log(blocked.text);
|
|
10062
|
+
process.exit(blocked.exitCode || 2);
|
|
10063
|
+
}
|
|
10064
|
+
const plan = consolidateCore(Object.assign({}, opts, { apply: false }));
|
|
10065
|
+
const groups = (plan.report && plan.report.groups) || [];
|
|
10066
|
+
const items = groups.reduce(function (s, g) { return s + g.count; }, 0);
|
|
10067
|
+
if (groups.length) {
|
|
10068
|
+
const token = groups.length + ' 组 / ' + items + ' 条';
|
|
10069
|
+
const typed = await promptLine('即将把 ' + token + ' 压缩为摘要(原文保留在 .archive/,可 --undo 回滚)。输入「' + token + '」确认: ');
|
|
10070
|
+
if (typed === null || String(typed).trim() !== token) {
|
|
10071
|
+
console.log('已取消,未做任何改动。');
|
|
10072
|
+
process.exit(2);
|
|
10073
|
+
}
|
|
10074
|
+
}
|
|
10075
|
+
opts = Object.assign({}, opts, { yes: true });
|
|
10076
|
+
}
|
|
9156
10077
|
const r = consolidateCore(opts);
|
|
9157
|
-
console.log(r.
|
|
10078
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
10079
|
+
else console.log(r.text);
|
|
10080
|
+
if (r.exitCode) process.exit(r.exitCode);
|
|
9158
10081
|
if (r.error) process.exit(2);
|
|
9159
10082
|
}
|
|
9160
10083
|
|
|
@@ -9447,6 +10370,10 @@ async function main() {
|
|
|
9447
10370
|
else if (a === '--yes') opts.yes = true;
|
|
9448
10371
|
else if (a === '--keep-memories') opts.keepMemories = true;
|
|
9449
10372
|
else if (a === '--keep-identity') opts.keepIdentity = true;
|
|
10373
|
+
else if (a === '--no-usage') opts.noUsage = true;
|
|
10374
|
+
else if (a === '--capacity') opts.capacity = true;
|
|
10375
|
+
else if (a === '--propose') opts.propose = true;
|
|
10376
|
+
else if (a === '--audit') opts.audit = true;
|
|
9450
10377
|
else if (valueOpts.has(a)) {
|
|
9451
10378
|
const v = args[++i];
|
|
9452
10379
|
if (a === '--type') opts.type = v;
|
|
@@ -9501,6 +10428,7 @@ async function main() {
|
|
|
9501
10428
|
else if (a === '--against') opts.against = v;
|
|
9502
10429
|
else if (a === '--template') opts.template = v;
|
|
9503
10430
|
else if (a === '--path') opts.path = v;
|
|
10431
|
+
else if (a === '--from') opts.from = v;
|
|
9504
10432
|
} else if (a.startsWith('--')) {
|
|
9505
10433
|
if (a === '--query') {
|
|
9506
10434
|
console.error('未知选项: --query。recall/search 的关键词是位置参数:yotta-memory recall [关键词]');
|
|
@@ -9557,8 +10485,9 @@ async function main() {
|
|
|
9557
10485
|
case 'scan': await cmdScan(opts); break;
|
|
9558
10486
|
case 'maintain': cmdMaintain(opts); break;
|
|
9559
10487
|
case 'distill': cmdDistill(opts); break;
|
|
9560
|
-
case 'consolidate': if (opts.undo === true && rest.length) opts.undo = rest[0]; cmdConsolidate(opts); break;
|
|
10488
|
+
case 'consolidate': if (opts.undo === true && rest.length) opts.undo = rest[0]; await cmdConsolidate(opts); break;
|
|
9561
10489
|
case 'forget': cmdForget(rest[0], opts); break;
|
|
10490
|
+
case 'rename': cmdRename(rest[0], rest[1], opts); break;
|
|
9562
10491
|
case 'archive': cmdArchive(opts); break;
|
|
9563
10492
|
case 'backup': {
|
|
9564
10493
|
const sub = rest[0];
|
|
@@ -9644,9 +10573,19 @@ module.exports = {
|
|
|
9644
10573
|
recallCore: recallCore,
|
|
9645
10574
|
profileCore: profileCore,
|
|
9646
10575
|
contextCore: contextCore,
|
|
10576
|
+
contextAuditCore: contextAuditCore,
|
|
9647
10577
|
importanceScore: importanceScore,
|
|
10578
|
+
usageSettings: usageSettings,
|
|
10579
|
+
usageTotals: usageTotals,
|
|
10580
|
+
usagePatchForMeta: usagePatchForMeta,
|
|
10581
|
+
applyUsageHits: applyUsageHits,
|
|
10582
|
+
parseHitDays: parseHitDays,
|
|
10583
|
+
getOwnerKeyFor: getOwnerKeyFor,
|
|
10584
|
+
loadOwnerIndex: loadOwnerIndex,
|
|
9648
10585
|
forgetCore: forgetCore,
|
|
10586
|
+
renameCore: renameCore,
|
|
9649
10587
|
archiveCore: archiveCore,
|
|
10588
|
+
capacityCore: capacityCore,
|
|
9650
10589
|
maintainCore: maintainCore,
|
|
9651
10590
|
consolidateCore: consolidateCore,
|
|
9652
10591
|
mergeCore: mergeCore,
|
|
@@ -9666,6 +10605,8 @@ module.exports = {
|
|
|
9666
10605
|
migrateLayout: migrateLayout,
|
|
9667
10606
|
loadIndexFor: loadIndexFor,
|
|
9668
10607
|
indexEntriesFor: indexEntriesFor,
|
|
10608
|
+
loadOwnerIndex: loadOwnerIndex,
|
|
10609
|
+
getOwnerKeyFor: getOwnerKeyFor,
|
|
9669
10610
|
normalizeYearList: normalizeYearList,
|
|
9670
10611
|
indexFingerprint: indexFingerprint,
|
|
9671
10612
|
benchCore: benchCore,
|
|
@@ -9688,6 +10629,10 @@ module.exports = {
|
|
|
9688
10629
|
SCALE_CONFIG_KEYS: SCALE_CONFIG_KEYS,
|
|
9689
10630
|
SCALE_THRESHOLD_DEFAULTS: SCALE_THRESHOLD_DEFAULTS,
|
|
9690
10631
|
scaleReport: scaleReport,
|
|
10632
|
+
USAGE_CONFIG_DEFAULTS: USAGE_CONFIG_DEFAULTS,
|
|
10633
|
+
usageSettings: usageSettings,
|
|
10634
|
+
usageTotals: usageTotals,
|
|
10635
|
+
applyUsageHits: applyUsageHits,
|
|
9691
10636
|
runtimeRoot: runtimeRoot,
|
|
9692
10637
|
runtimeManifestPath: runtimeManifestPath,
|
|
9693
10638
|
runtimeVersionsDir: runtimeVersionsDir,
|