@yottameta/yotta-memory 0.18.0 → 0.19.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.
@@ -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.18.0';
28
+ const VERSION = '0.19.0';
29
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']);
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', '--rules', '--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 || '' };
@@ -118,6 +118,7 @@ const HELP_MODEL = [
118
118
  ] },
119
119
  { name: 'maintain', usage: 'maintain [选项]', what: '记忆自组织:归档、遗忘候选、去重和合并', when: '定期清理低价值记忆、合并重复条目时', options: [
120
120
  helpOption('--capacity', '', '查看容量水位与淘汰 / 晋升建议', '库变大,想看哪些条目值得清理或提升时', '只读报告,不会自动归档或改权重'),
121
+ helpOption('--rules', '', '查看重复踩坑 → 建议升级为规则的清单', '同一类问题反复出现、想把它固化成 BOUND 规则时', '只读报告,不自动写规则;阈值走 config maintain_rule_min_hits'),
121
122
  helpOption('--dedup', '', '只做重复检查并给置信度', '想先看哪些条目重复时', ''),
122
123
  helpOption('--apply', '', '真正执行,而不是只预览', '确认预览结果后要落盘时', '会改动记忆库并创建事务快照;先跑 dry-run'),
123
124
  helpOption('--purge', '', '对遗忘候选执行真删', '确认候选确实不再需要时', '真删不可逆;通常先用归档而不是 purge'),
@@ -146,6 +147,8 @@ const HELP_MODEL = [
146
147
  { name: 'distill', usage: 'distill [--model <命令>] [选项]', what: '把心理日志蒸馏成统计摘要、主题画像和知识地图', when: '想把长期记录整理成更高层的自我画像时', options: [
147
148
  helpOption('--model', '<命令>', '调用本地外部模型辅助蒸馏', '需要更高质量摘要、且已准备本地模型命令时', '命令在本地执行;不要指向会外传原文的服务'),
148
149
  helpOption('--subject', '<标题>', '指定蒸馏输出主题', '想给这次摘要一个固定标题时', ''),
150
+ helpOption('--owner', '<id>', '指定蒸馏归属的智能体', '想蒸馏某个 owner 的记忆时', '默认当前身份;跨 owner 私密仍按 owner 门跳过'),
151
+ helpOption('--out', '<路径>', '把蒸馏报告导出到指定路径', '想把报告放到自定义位置时', '不指定时按归属写入 distills 目录'),
149
152
  helpOption('--json', '', '输出 JSON', '自动化处理蒸馏结果时', ''),
150
153
  ] },
151
154
  { name: 'feedback', usage: 'feedback <记忆 id> [--useful | --useless | --undo]', what: '记录记忆是否有用', when: '一条记忆刚刚帮助了你,或明显误导了你时', options: [
@@ -207,6 +210,7 @@ const HELP_MODEL = [
207
210
  helpOption('--audit', '', '审计被压缩掉的内容是否已经落盘', '宿主压缩上下文后,想确认有没有决策只存在于对话里时', '只读:只打印 remember 建议,不会自动补写'),
208
211
  helpOption('--from', '<文件|->', '指定待审计内容(- 表示标准输入)', '把宿主压缩摘要或丢弃段落喂给审计时', '不传时审计当前上下文包的丢弃清单'),
209
212
  helpOption('--limit', '<条数>', '限制近期记忆条数', '上下文太长、想缩短时', ''),
213
+ helpOption('--json', '', '输出结构化 JSON(hook 块 + text)', '脚本读取上下文包或扩展装载状态时', '未配置扩展提供方时也返回 JSON'),
210
214
  helpOption('--owner', '<id>', '指定 owner 范围', '需要看某个 owner 的上下文时', '仍然受私密区权限约束'),
211
215
  helpOption('--budget', '<字符数>', '设置动态记忆字符预算', '需要控制上下文包大小时', '0 表示不限制'),
212
216
  helpOption('--focus', '<关键词>', '按当前任务聚焦', '这次任务很明确、想优先拉相关记忆时', ''),
@@ -5233,12 +5237,100 @@ function subjectsSimilar(a, b) {
5233
5237
  return editDistance(sa, sb) <= 3;
5234
5238
  }
5235
5239
  // 记忆自组织:规则层自动归档/遗忘/去重(默认 dry-run,--apply 才执行,--purge 才真删)
5240
+ // ---- v0.18.1 A10:重复踩坑 → 建议升级为规则(只读报告)----
5241
+ function normalizeRuleSubject(s) {
5242
+ return String(s || '').toLowerCase().replace(/[^\u4e00-\u9fa5a-z]+/g, '');
5243
+ }
5244
+ function ruleGroupKeyFor(entry) {
5245
+ for (const t of (entry.tags || [])) {
5246
+ const m = /^pattern-key:(.+)$/i.exec(String(t));
5247
+ if (m) return { key: 'pattern-key:' + m[1].trim(), kind: 'pattern-key' };
5248
+ }
5249
+ const sm = /^yotta-learn:\s*(.+)$/i.exec(String(entry.subject || ''));
5250
+ const cm = /^\[([a-z_]+)\]/i.exec(String(entry.statement || '').trim());
5251
+ if (sm && cm) return { key: cm[1].toLowerCase() + '@' + sm[1].trim(), kind: 'yotta-learn' };
5252
+ const domains = (entry.tags || []).filter(function (t) {
5253
+ return !distillHasWord(t, DISTILL_LESSON_WORDS) && !/^pattern-key:/i.test(String(t)) && String(t) !== '规则' && String(t) !== '边界';
5254
+ }).map(function (t) { return String(t).toLowerCase(); }).sort();
5255
+ if (domains.length) return { key: domains.join('+'), kind: 'tags' };
5256
+ return { key: normalizeRuleSubject(entry.subject), kind: 'subject' };
5257
+ }
5258
+ function isRuleLessonCandidate(entry) {
5259
+ const hay = (entry.tags || []).join(',') + ' ' + String(entry.subject || '');
5260
+ if (distillHasWord(hay, DISTILL_LESSON_WORDS)) return true;
5261
+ const head = String(entry.statement || '').trim().slice(0, 40);
5262
+ return /^\[([a-z_]+)\]/i.test(head) && distillHasWord(head, DISTILL_LESSON_WORDS);
5263
+ }
5264
+ function ruleMiningCore(root, opts) {
5265
+ opts = opts || {};
5266
+ const cfg = loadConfig();
5267
+ const rawMin = (opts.ruleMinHits !== undefined && opts.ruleMinHits !== null) ? opts.ruleMinHits : cfg.maintain_rule_min_hits;
5268
+ const minHits = Math.max(2, parseInt(rawMin || '3', 10) || 3);
5269
+ const selfAgent = resolveIdentity(opts).id;
5270
+ const buckets = {};
5271
+ let scanned = 0, candidates = 0, denied = 0;
5272
+ for (const fp of collectEntryFiles(root)) {
5273
+ const rel = relOf(root, fp);
5274
+ if (checkOwnerWritable(root, rel, selfAgent, opts.unsafe)) { denied += 1; continue; }
5275
+ let e;
5276
+ try { e = readEntry(fp, root); } catch (err) { continue; }
5277
+ scanned += 1;
5278
+ if (['COMMIT', 'FACT', 'PREF'].indexOf(e.type) === -1) continue;
5279
+ if (!isRuleLessonCandidate(e)) continue;
5280
+ const gk = ruleGroupKeyFor(e);
5281
+ if (!gk.key) continue;
5282
+ candidates += 1;
5283
+ (buckets[gk.key] = buckets[gk.key] || []).push({ e: e, kind: gk.kind, file: rel });
5284
+ }
5285
+ const groups = Object.keys(buckets).map(function (key) {
5286
+ const items = buckets[key].slice().sort(function (a, b) {
5287
+ return String(b.e.updated || b.e.created || '').localeCompare(String(a.e.updated || a.e.created || ''));
5288
+ });
5289
+ const best = items.slice().sort(function (a, b) { return (b.e.confidence || 0) - (a.e.confidence || 0); })[0].e;
5290
+ const dates = items.map(function (x) { return x.e.created || x.e.updated || ''; }).filter(Boolean).sort();
5291
+ return {
5292
+ key: key,
5293
+ kind: items[0].kind,
5294
+ count: items.length,
5295
+ files: items.map(function (x) { return x.file; }),
5296
+ span: dates.length ? dates[0] + ' ~ ' + dates[dates.length - 1] : '',
5297
+ best: best,
5298
+ };
5299
+ }).filter(function (g) { return g.count >= minHits; }).sort(function (a, b) {
5300
+ return b.count - a.count || a.key.localeCompare(b.key);
5301
+ });
5302
+ return { error: false, groups: groups, scanned: scanned, candidates: candidates, denied: denied, minHits: minHits };
5303
+ }
5304
+ function ruleMiningLines(result) {
5305
+ const lines = ['### 建议升级为规则(只读,不自动写入)'];
5306
+ if (!result.groups.length) {
5307
+ lines.push('- (无重复踩坑组达到阈值:同组 ≥ ' + result.minHits + ' 条;可用 config set maintain_rule_min_hits N 调整)');
5308
+ return lines;
5309
+ }
5310
+ for (const g of result.groups) {
5311
+ lines.push('- 组键: ' + g.key + '(' + g.count + ' 条' + (g.span ? ',' + g.span : '') + ')');
5312
+ lines.push(' 代表: [' + g.best.type + '] ' + (g.best.subject || '') + ' — ' + String(g.best.statement || '').slice(0, 60));
5313
+ lines.push(' 建议: 人工确认后执行 yotta-memory remember BOUND "规则:' + g.key + '" "<陈述>"');
5314
+ }
5315
+ lines.push('- 说明: 元忆只给晋升信号,不自动写 BOUND;与元习 .learnings/ 的分工见 SKILL「记忆守则」。');
5316
+ return lines;
5317
+ }
5318
+
5236
5319
  function maintainCore(opts) {
5237
5320
  opts = opts || {};
5238
5321
  const apply = !!opts.apply;
5239
5322
  const purge = !!opts.purge;
5240
5323
  const root = userRoot();
5241
5324
  if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
5325
+ if (opts.rules && (apply || purge || opts.dedup)) {
5326
+ return { error: true, exitCode: 2, text: '拒绝: --rules 是只读筛选,不能与 --apply / --purge / --dedup 同时使用。' };
5327
+ }
5328
+ if (opts.rules) {
5329
+ const lines = ['## yotta-memory maintain --rules(重复踩坑 → 规则晋升建议)', ''];
5330
+ const result = ruleMiningCore(root, opts);
5331
+ lines.push.apply(lines, ruleMiningLines(result));
5332
+ return { error: false, text: lines.join('\n'), report: result };
5333
+ }
5242
5334
  let guard = null;
5243
5335
  if (apply) {
5244
5336
  guard = destructiveGuardCore({
@@ -5347,6 +5439,13 @@ function maintainCore(opts) {
5347
5439
  lines.push('');
5348
5440
  lines.push('- 跨 owner 私密条目已跳过(' + deniedCrossOwner + ' 条):请用 --agent <id> 声明自己的身份;只有用户显式授权(--unsafe)才能越界维护。');
5349
5441
  }
5442
+ if (!opts.dedup) {
5443
+ const ruleResult = ruleMiningCore(root, opts);
5444
+ if (ruleResult.groups.length) {
5445
+ lines.push('');
5446
+ lines.push.apply(lines, ruleMiningLines(ruleResult));
5447
+ }
5448
+ }
5350
5449
  // 去重(v0.10.0:置信度分档 + --apply 自动合并;--dedup 与归档/遗忘互斥)
5351
5450
  if (opts.dedup) appendDedupBlock(lines, root, opts);
5352
5451
  return { error: false, text: lines.join('\n') };
@@ -5393,7 +5492,263 @@ function mergeCore(refA, refB, opts) {
5393
5492
  appendAudit(root, 'audit', { ts: new Date().toISOString(), file: drop.file, action: 'merge', reason: '并入 ' + keep.file, utility: round3(utilityScore(keep)) });
5394
5493
  return { error: false, text: '已合并: ' + drop.file + ' -> ' + keep.file + '\n 保留: [' + keep.type + '] ' + keep.subject + ': ' + keep.statement + '\n 归档低 confidence: ' + drop.file + '\n 事务快照: ' + guard.snapshot.id };
5395
5494
  }
5396
- // 心理日志蒸馏:统计摘要 / 主题画像 / 知识地图(启发式默认,可选 --model)
5495
+ // ---- v0.18.1 A8:蒸馏分类型提取 / 溯源锚点 / 质量指标(确定性,零依赖)----
5496
+ const DISTILL_CLASS_NAMES = ['教训', '待办', '成长', '事件', '规则边界', '其他'];
5497
+ const DISTILL_LESSON_WORDS = ['教训', '踩坑', '失误', '错误', '故障', '事故', '纠正', 'learning', 'error', 'correction', 'incident'];
5498
+ const DISTILL_TASK_WORDS = ['待办', '任务', 'action', 'next', '跟进'];
5499
+ const DISTILL_TASK_TAG_RE = /^t(o)?do$/i;
5500
+ const DISTILL_GROWTH_WORDS = ['成长', '改进', '提升', '洞见', '最佳实践', '复盘', 'insight', 'growth', 'improvement', 'best-practice'];
5501
+ const DISTILL_EVENT_WORDS = ['事件', '里程碑', '发布', '上线', '会议', '活动', 'release', 'milestone'];
5502
+ const DISTILL_FIELD_LIMIT = 500;
5503
+ function distillHasWord(haystack, words) {
5504
+ const s = String(haystack || '').toLowerCase();
5505
+ for (const w of words) if (s.indexOf(String(w).toLowerCase()) !== -1) return true;
5506
+ return false;
5507
+ }
5508
+ function distillClassify(entry, meta) {
5509
+ if (!meta || !meta.type) return '其他';
5510
+ if (entry.type === 'BOUND') return '规则边界';
5511
+ const hay = (entry.tags || []).join(',') + ' ' + String(entry.subject || '');
5512
+ if (distillHasWord(hay, DISTILL_LESSON_WORDS)) return '教训';
5513
+ const hasTaskTag = (entry.tags || []).some(function (t) { return DISTILL_TASK_TAG_RE.test(String(t).trim()); });
5514
+ if (distillHasWord(hay, DISTILL_TASK_WORDS) || hasTaskTag) return '待办';
5515
+ if (distillHasWord(hay, DISTILL_GROWTH_WORDS)) return '成长';
5516
+ if (distillHasWord(hay, DISTILL_EVENT_WORDS)) return '事件';
5517
+ if (entry.type === 'COMMIT') return '待办';
5518
+ if (entry.type === 'PREF') return '成长';
5519
+ if (entry.type === 'FACT') return '事件';
5520
+ return '其他';
5521
+ }
5522
+ function distillSentences(body) {
5523
+ return String(body || '').replace(/([。!?])/g, '$1\n').split('\n').map(function (s) { return s.trim(); }).filter(Boolean);
5524
+ }
5525
+ function distillLineOf(lines, needle) {
5526
+ const n = String(needle || '');
5527
+ if (!n) return 0;
5528
+ for (let i = 0; i < lines.length; i++) if (lines[i].indexOf(n) !== -1) return i + 1;
5529
+ return 0;
5530
+ }
5531
+ function distillClamp(value, limit) {
5532
+ const s = String(value === undefined || value === null ? '' : value);
5533
+ const max = limit || DISTILL_FIELD_LIMIT;
5534
+ if (s.length <= max) return { value: s, truncated: false };
5535
+ return { value: s.slice(0, max) + '…(截断 ' + (s.length - max) + ' 字符,全文见溯源)', truncated: true };
5536
+ }
5537
+ function distillDateNear(sentences, body) {
5538
+ const dateRe = /\d{4}-\d{2}-\d{2}/;
5539
+ for (const s of sentences) {
5540
+ if (dateRe.test(s) && /(截止|前完成|之前|deadline|due)/i.test(s)) return s.match(dateRe)[0];
5541
+ }
5542
+ const all = String(body || '');
5543
+ if (/(截止|前完成|deadline|due)/i.test(all)) {
5544
+ const m = all.match(dateRe);
5545
+ if (m) return m[0];
5546
+ }
5547
+ return '';
5548
+ }
5549
+ function distillExtract(entry, cls, body, lines) {
5550
+ const sents = distillSentences(body);
5551
+ const statement = String(entry.statement || '');
5552
+ const subject = String(entry.subject || '');
5553
+ const tags = (entry.tags || []).join(', ');
5554
+ const fields = {};
5555
+ const used = [];
5556
+ const take = function (name, value) {
5557
+ const ok = value !== undefined && value !== null && String(value) !== '';
5558
+ fields[name] = ok ? String(value) : '未记录';
5559
+ if (ok) used.push(String(value));
5560
+ };
5561
+ let fieldNames = [];
5562
+ if (cls === '教训') {
5563
+ fieldNames = ['教训', '触发条件', '规避方法'];
5564
+ const avoid = sents.filter(function (s) { return /(避免|应该|下次|教训)/.test(s); })[0];
5565
+ take('教训', statement);
5566
+ take('触发条件', tags || subject);
5567
+ take('规避方法', avoid);
5568
+ if (avoid) used.push(avoid);
5569
+ } else if (cls === '待办') {
5570
+ fieldNames = ['任务', '优先级', '截止'];
5571
+ let priority = '';
5572
+ for (const t of (entry.tags || [])) if (/^(high|medium|low|高|中|低)$/i.test(String(t))) { priority = t; break; }
5573
+ const due = distillDateNear(sents.concat([statement]), body + ' ' + statement);
5574
+ take('任务', statement);
5575
+ take('优先级', priority);
5576
+ take('截止', due);
5577
+ if (due) used.push(due);
5578
+ } else if (cls === '成长') {
5579
+ fieldNames = ['改进点', '前后对比', '效果'];
5580
+ const before = sents.filter(function (s) { return /(之前|后来|从.+到)/.test(s); })[0];
5581
+ const effect = sents.filter(function (s) { return /(效果|提升|%|更好)/.test(s); })[0];
5582
+ take('改进点', statement);
5583
+ take('前后对比', before);
5584
+ take('效果', effect);
5585
+ if (before) used.push(before);
5586
+ if (effect) used.push(effect);
5587
+ } else if (cls === '事件') {
5588
+ fieldNames = ['时间', '内容', '结果'];
5589
+ const last = sents.length ? sents[sents.length - 1] : '';
5590
+ take('时间', entry.created || entry.updated);
5591
+ take('内容', subject + (statement ? ': ' + statement : ''));
5592
+ take('结果', last);
5593
+ if (last) used.push(last);
5594
+ } else if (cls === '规则边界') {
5595
+ fieldNames = ['规则', '适用范围', '例外'];
5596
+ const ex = sents.filter(function (s) { return /(除非|例外)/.test(s); })[0];
5597
+ take('规则', statement);
5598
+ take('适用范围', tags || subject);
5599
+ take('例外', ex);
5600
+ if (ex) used.push(ex);
5601
+ } else {
5602
+ fieldNames = ['内容'];
5603
+ take('内容', statement || (sents[0] || String(body || '').trim().slice(0, 200)));
5604
+ }
5605
+ const lineNumbers = used.map(function (v) { return distillLineOf(lines, v); }).filter(Boolean);
5606
+ if (!lineNumbers.length) {
5607
+ const fallback = distillLineOf(lines, statement) || distillLineOf(lines, subject) || 1;
5608
+ lineNumbers.push(fallback);
5609
+ }
5610
+ let truncated = false;
5611
+ for (const name of fieldNames) {
5612
+ if (fields[name] === '未记录') continue;
5613
+ const clamped = distillClamp(fields[name]);
5614
+ if (clamped.truncated) { fields[name] = clamped.value; truncated = true; }
5615
+ }
5616
+ return {
5617
+ fields: fields,
5618
+ fieldNames: fieldNames,
5619
+ anchor: { start: Math.min.apply(null, lineNumbers), end: Math.max.apply(null, lineNumbers) },
5620
+ truncated: truncated,
5621
+ };
5622
+ }
5623
+ function distillElementStats(entry, cls, body) {
5624
+ const sents = distillSentences(body);
5625
+ const tags = (entry.tags || []).join(', ');
5626
+ const hasPriority = (entry.tags || []).some(function (t) { return /^(high|medium|low|高|中|低)$/i.test(String(t)); });
5627
+ let specs = [];
5628
+ if (cls === '教训') specs = [!!entry.statement, !!(tags || entry.subject), /(避免|应该|下次|教训)/.test(body)];
5629
+ else if (cls === '待办') specs = [!!entry.statement, hasPriority, !!distillDateNear(sents.concat([String(entry.statement || '')]), body + ' ' + String(entry.statement || ''))];
5630
+ else if (cls === '成长') specs = [!!entry.statement, /(之前|后来|从.+到)/.test(body), /(效果|提升|%|更好)/.test(body)];
5631
+ else if (cls === '事件') specs = [!!(entry.created || entry.updated), !!(entry.subject || entry.statement), sents.length > 0];
5632
+ else if (cls === '规则边界') specs = [!!entry.statement, !!(tags || entry.subject), /(除非|例外)/.test(body)];
5633
+ else specs = [!!(entry.statement || body)];
5634
+ const detected = specs.filter(Boolean).length;
5635
+ return { detected: detected, expected: specs.length };
5636
+ }
5637
+ function distillAnalyze(root, entries, owner) {
5638
+ const classes = {};
5639
+ for (const name of DISTILL_CLASS_NAMES) classes[name] = { name: name, count: 0, items: [] };
5640
+ const skipped = [];
5641
+ let analyzed = 0, entriesWithItems = 0, itemCount = 0, traceOk = 0;
5642
+ let sourceChars = 0, extractedChars = 0, detected = 0, filled = 0;
5643
+ const refusedRe = /\uFFFD/;
5644
+ for (const e of entries) {
5645
+ const entryRoot = e._root || root;
5646
+ const fp = path.join(entryRoot, e.file);
5647
+ const entryOwner = ownerFromPrivatePath(entryRoot, fp) || owner;
5648
+ let raw;
5649
+ try {
5650
+ raw = readMemoryText(entryRoot, fp, entryOwner);
5651
+ } catch (err) {
5652
+ skipped.push({ file: e.file, reason: 'encoding', detail: String(err.message || err) });
5653
+ continue;
5654
+ }
5655
+ raw = String(raw).replace(/^\uFEFF/, '');
5656
+ if (!raw.trim()) {
5657
+ skipped.push({ file: e.file, reason: 'empty', detail: '' });
5658
+ continue;
5659
+ }
5660
+ if (refusedRe.test(raw)) {
5661
+ skipped.push({ file: e.file, reason: 'encoding', detail: 'UTF-8 解码失败' });
5662
+ continue;
5663
+ }
5664
+ const parsed = parseFrontmatter(raw);
5665
+ const hasMeta = !!(parsed.meta && parsed.meta.type);
5666
+ const local = Object.assign({}, e);
5667
+ if (hasMeta) {
5668
+ local.type = String(parsed.meta.type || e.type).toUpperCase();
5669
+ local.subject = parsed.meta.subject || e.subject;
5670
+ local.statement = parsed.meta.statement || e.statement;
5671
+ local.tags = parseTags(parsed.meta.tags);
5672
+ local.created = parsed.meta.created || e.created;
5673
+ local.updated = parsed.meta.updated || e.updated;
5674
+ } else {
5675
+ local.type = '';
5676
+ local.tags = [];
5677
+ local.subject = path.basename(e.file).replace(/\.md(\.enc)?$/, '');
5678
+ local.statement = String(parsed.body || raw).trim().split('\n')[0].slice(0, 200);
5679
+ skipped.push({ file: e.file, reason: 'no-structure', detail: '按裸文本处理', analyzed: true });
5680
+ }
5681
+ const cls = distillClassify(local, hasMeta ? parsed.meta : null);
5682
+ const body = String(parsed.body || '');
5683
+ const lines = raw.split('\n');
5684
+ const ex = distillExtract(local, cls, body, lines);
5685
+ const stats = distillElementStats(local, cls, body);
5686
+ detected += stats.detected;
5687
+ filled += ex.fieldNames.filter(function (n) { return ex.fields[n] !== '未记录'; }).length;
5688
+ const anchorText = '[溯源: ' + e.file + '#L' + ex.anchor.start + '-L' + ex.anchor.end + ']';
5689
+ const item = {
5690
+ ref: e.file,
5691
+ type: local.type || 'UNKNOWN',
5692
+ subject: local.subject,
5693
+ statement: local.statement,
5694
+ fields: ex.fields,
5695
+ anchor: e.file + '#L' + ex.anchor.start + '-L' + ex.anchor.end,
5696
+ anchorText: anchorText,
5697
+ truncated: ex.truncated,
5698
+ };
5699
+ classes[cls].items.push(item);
5700
+ classes[cls].count += 1;
5701
+ analyzed += 1;
5702
+ entriesWithItems += 1;
5703
+ itemCount += 1;
5704
+ traceOk += 1;
5705
+ sourceChars += String(local.statement || '').length + body.length;
5706
+ for (const n of ex.fieldNames) if (ex.fields[n] !== '未记录') extractedChars += String(ex.fields[n]).length;
5707
+ if (ex.truncated) skipped.push({ file: e.file, reason: 'truncated', detail: '超长字段已截断到 ' + DISTILL_FIELD_LIMIT + ' 字符', analyzed: true });
5708
+ }
5709
+ const mainLines = ['## 二、分类型提取清单', ''];
5710
+ if (!itemCount) mainLines.push('(无可提取条目)');
5711
+ for (const name of DISTILL_CLASS_NAMES) {
5712
+ const c = classes[name];
5713
+ if (!c.count) continue;
5714
+ mainLines.push('### ' + name + '(' + c.count + ' 条)');
5715
+ for (const item of c.items) {
5716
+ mainLines.push('- [' + item.type + '] ' + item.subject);
5717
+ for (const fn of Object.keys(item.fields)) mainLines.push(' - ' + fn + ': ' + item.fields[fn]);
5718
+ mainLines.push(' - ' + item.anchorText);
5719
+ mainLines.push('');
5720
+ }
5721
+ }
5722
+ const compression = extractedChars > 0 ? Math.round((sourceChars / extractedChars) * 100) / 100 : 0;
5723
+ const entryCoverage = analyzed > 0 ? round4(entriesWithItems / analyzed) : 1;
5724
+ const traceCoverage = itemCount > 0 ? round4(traceOk / itemCount) : 1;
5725
+ const elementRate = detected > 0 ? round4(filled / detected) : 1;
5726
+ mainLines.push('## 三、质量指标', '');
5727
+ mainLines.push('- 源正文字符: ' + sourceChars + ' / 提取要点字符: ' + extractedChars + ' → 实测压缩比 ' + compression + '(只报本次实测,不承诺倍数)');
5728
+ mainLines.push('- 条目覆盖率: ' + Math.round(entryCoverage * 100) + '%(' + entriesWithItems + '/' + analyzed + ')');
5729
+ mainLines.push('- 溯源覆盖率: ' + Math.round(traceCoverage * 100) + '%(' + traceOk + '/' + itemCount + ',目标 100%)');
5730
+ mainLines.push('- 要素提取率: ' + Math.round(elementRate * 100) + '%(' + filled + '/' + detected + ',确定性回归护栏,不是语义保真承诺)');
5731
+ const tailLines = ['## 六、跳过与边界', ''];
5732
+ if (!skipped.length) tailLines.push('(无跳过项)');
5733
+ for (const s of skipped) {
5734
+ tailLines.push('- ' + s.file + ': ' + s.reason + (s.detail ? '(' + s.detail + ')' : '') + (s.analyzed ? '(已分析)' : '(已跳过)'));
5735
+ }
5736
+ return {
5737
+ classes: DISTILL_CLASS_NAMES.map(function (n) { return classes[n]; }).filter(function (c) { return c.count > 0; }),
5738
+ skipped: skipped,
5739
+ mainLines: mainLines,
5740
+ tailLines: tailLines,
5741
+ analyzed: analyzed,
5742
+ metrics: {
5743
+ compression_ratio: compression,
5744
+ entry_coverage: entryCoverage,
5745
+ trace_coverage: traceCoverage,
5746
+ element_extract_rate: elementRate,
5747
+ },
5748
+ };
5749
+ }
5750
+
5751
+ // 心理日志蒸馏:统计摘要 / 分类型提取 / 质量指标 / 主题画像 / 知识地图(启发式默认,可选 --model)
5397
5752
  function distillCore(opts) {
5398
5753
  opts = opts || {};
5399
5754
  const root = userRoot();
@@ -5404,15 +5759,16 @@ function distillCore(opts) {
5404
5759
  for (const r of memoryRoots()) {
5405
5760
  for (const e of ensureIndex(r)) {
5406
5761
  if (classifyRead(e, owner, '', !!opts.unsafe, selfAgent) === 'denied') continue;
5407
- entries.push(e);
5762
+ entries.push(Object.assign({}, e, { _root: r }));
5408
5763
  }
5409
5764
  }
5410
5765
  const lines = [];
5411
- lines.push('# 元忆记忆蒸馏报告(v0.8.0)');
5766
+ lines.push('# 元忆记忆蒸馏报告(v' + VERSION + ')');
5412
5767
  lines.push('');
5413
5768
  lines.push('> 生成: yotta-memory distill | 启发式蒸馏(零依赖);--model <cmd> 可选外部模型增强');
5414
5769
  lines.push('> 时间: ' + new Date().toISOString() + ' / 可读条目: ' + entries.length + ' 条');
5415
5770
  lines.push('');
5771
+ const analysis = distillAnalyze(root, entries, owner);
5416
5772
  // 1) 统计摘要
5417
5773
  lines.push('## 一、统计摘要');
5418
5774
  lines.push('');
@@ -5434,8 +5790,10 @@ function distillCore(opts) {
5434
5790
  for (const e of entries) { const n = e.feedback_net || 0; fbSum += n; if (n > 0) usefulN++; else if (n < 0) uselessN++; }
5435
5791
  lines.push('- 反馈统计: feedback_net 总和 ' + fbSum + '(正 ' + usefulN + ' 条 / 负 ' + uselessN + ' 条)');
5436
5792
  lines.push('');
5793
+ lines.push.apply(lines, analysis.mainLines);
5794
+ lines.push('');
5437
5795
  // 2) 主题画像(按 subject 聚类合并)
5438
- lines.push('## 二、主题画像(按 subject 聚类,启发式合并)');
5796
+ lines.push('## 四、主题画像(按 subject 聚类,启发式合并)');
5439
5797
  lines.push('');
5440
5798
  const groups = {};
5441
5799
  for (const e of entries) {
@@ -5454,7 +5812,7 @@ function distillCore(opts) {
5454
5812
  lines.push('');
5455
5813
  }
5456
5814
  // 3) 知识地图(type + tags 层级)
5457
- lines.push('## 三、知识地图(type → tags)');
5815
+ lines.push('## 五、知识地图(type → tags)');
5458
5816
  lines.push('');
5459
5817
  const map = {};
5460
5818
  for (const e of entries) {
@@ -5469,6 +5827,8 @@ function distillCore(opts) {
5469
5827
  lines.push('- ' + tk + ': ' + map[t][tk].map(function (e) { return e.subject; }).join(' / '));
5470
5828
  }
5471
5829
  }
5830
+ lines.push('');
5831
+ lines.push.apply(lines, analysis.tailLines);
5472
5832
  let body = lines.join('\n') + '\n';
5473
5833
  // 可选模型增强:--model <cmd>,stdin 传结构化摘要,stdout 取提炼文本
5474
5834
  let modelOut = '';
@@ -5478,12 +5838,24 @@ function distillCore(opts) {
5478
5838
  const r = runDistillModel(String(opts.model), payload);
5479
5839
  if (r.stdout) modelOut = String(r.stdout).trim();
5480
5840
  if (!modelOut) modelOut = '(模型无输出)';
5481
- body += '\n## 四、模型提炼(--model ' + opts.model + ')\n\n' + modelOut + '\n';
5841
+ body += '\n## 七、模型提炼(--model ' + opts.model + ')\n\n' + modelOut + '\n';
5482
5842
  } catch (e) {
5483
5843
  modelOut = '(模型调用失败: ' + e.message + ')';
5484
- body += '\n## 四、模型提炼\n\n' + modelOut + '\n';
5844
+ body += '\n## 七、模型提炼\n\n' + modelOut + '\n';
5485
5845
  }
5486
5846
  }
5847
+ const makeReport = function (writtenPath) {
5848
+ return {
5849
+ schemaVersion: 1,
5850
+ generated: new Date().toISOString(),
5851
+ root: root,
5852
+ scope: { owner: owner, entries: entries.length, analyzed: analysis.analyzed },
5853
+ classes: analysis.classes,
5854
+ metrics: analysis.metrics,
5855
+ skipped: analysis.skipped,
5856
+ written: writtenPath || '',
5857
+ };
5858
+ };
5487
5859
  // 落盘:--out 指定 / 私密 distills / 公共 facts/distills
5488
5860
  let written = '';
5489
5861
  try {
@@ -5514,9 +5886,9 @@ function distillCore(opts) {
5514
5886
  }
5515
5887
  }
5516
5888
  } catch (e) {
5517
- return { error: false, text: lines.join('\n') + '\n\n[落盘失败] ' + e.message };
5889
+ return { error: false, text: lines.join('\n') + '\n\n[落盘失败] ' + e.message, report: makeReport('') };
5518
5890
  }
5519
- return { error: false, text: body + '\n[已写入] ' + written };
5891
+ return { error: false, text: body + '\n[已写入] ' + written, report: makeReport(written) };
5520
5892
  }
5521
5893
 
5522
5894
  function promptPassword(promptText) {
@@ -6639,7 +7011,8 @@ function cmdMaintain(opts) {
6639
7011
  }
6640
7012
  function cmdDistill(opts) {
6641
7013
  const r = distillCore(opts);
6642
- console.log(r.text);
7014
+ if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
7015
+ else console.log(r.text);
6643
7016
  if (r.error) process.exit(2);
6644
7017
  }
6645
7018
  function cmdForget(fileRef, opts) {
@@ -7189,6 +7562,109 @@ function compareContextRecent(a, b) {
7189
7562
  if (byTime !== 0) return byTime;
7190
7563
  return (b.access_count || 0) - (a.access_count || 0);
7191
7564
  }
7565
+
7566
+ const MEMORY_HOOK_MAX_CANDIDATES = 500;
7567
+ const MEMORY_HOOK_STATEMENT_LIMIT = 2000;
7568
+
7569
+ function hookBlock(status, providerId, note) {
7570
+ return {
7571
+ status: status || 'not_installed',
7572
+ provider_id: providerId || '',
7573
+ applied: false,
7574
+ evicted: [],
7575
+ dropped: [],
7576
+ note: note || '',
7577
+ };
7578
+ }
7579
+
7580
+ function hookStatusText(hook) {
7581
+ if (!hook) return '';
7582
+ if (hook.status === 'active') {
7583
+ const who = hook.provider_id ? '提供方 ' + hook.provider_id : '提供方';
7584
+ return '已应用(' + who + ';驱逐 ' + hook.evicted.length + ' / 丢弃 ' + hook.dropped.length + ')';
7585
+ }
7586
+ if (hook.status === 'license_required') return '需授权(该能力需要授权后使用;普通记忆不受影响)';
7587
+ if (hook.status === 'timeout') return '未生效(提供方超时,已回落普通记忆)';
7588
+ if (hook.status === 'invalid_output') return '未生效(提供方输出无效,已回落普通记忆)';
7589
+ if (hook.status === 'error') return '未生效(提供方异常,已回落普通记忆)';
7590
+ return hook.status;
7591
+ }
7592
+
7593
+ /**
7594
+ * memory.hook 装载点:只把「可驱逐」条目(非 BOUND / COMMIT)交给 provider。
7595
+ * provider 只能返回本次候选集内的 evict(驱逐清单);complete=true 时可选 selected(白名单)。
7596
+ * 身份画像、BOUND / COMMIT、预算 / 去重 / 宽限始终由引擎掌控;任何失败都走普通记忆。
7597
+ * 协议见 references/provider-protocol.md。
7598
+ */
7599
+ function applyMemoryHook(readableEntries, opts) {
7600
+ const block = hookBlock('not_installed', '', '');
7601
+ const pageable = readableEntries
7602
+ .filter(function (entry) { return entry.type !== 'BOUND' && entry.type !== 'COMMIT'; })
7603
+ .sort(compareContextRecent);
7604
+ const truncated = pageable.length > MEMORY_HOOK_MAX_CANDIDATES;
7605
+ const payload = {
7606
+ agent: opts.owner || '',
7607
+ budget: opts.budget || 0,
7608
+ focus: opts.focus || '',
7609
+ truncated: truncated,
7610
+ candidates: pageable.slice(0, MEMORY_HOOK_MAX_CANDIDATES).map(function (entry) {
7611
+ return {
7612
+ file: entry.file,
7613
+ type: entry.type,
7614
+ subject: entry.subject,
7615
+ statement: String(entry.statement || '').slice(0, MEMORY_HOOK_STATEMENT_LIMIT),
7616
+ created: entry.created || '',
7617
+ updated: entry.updated || '',
7618
+ };
7619
+ }),
7620
+ };
7621
+ let run;
7622
+ try {
7623
+ run = require('./provider').runCapability('memory.hook', payload);
7624
+ } catch (e) {
7625
+ block.status = 'error';
7626
+ block.note = '扩展装载失败:' + e.message;
7627
+ return { block: block, evict: new Set(), selected: null };
7628
+ }
7629
+ block.status = run.status;
7630
+ block.provider_id = run.provider_id || '';
7631
+ block.note = run.note || run.message || '';
7632
+ if (run.status !== 'active' || !run.data || typeof run.data !== 'object') {
7633
+ return { block: block, evict: new Set(), selected: null };
7634
+ }
7635
+ const sent = new Set(payload.candidates.map(function (item) { return item.file; }));
7636
+ const evicted = [];
7637
+ for (const item of (Array.isArray(run.data.evict) ? run.data.evict : [])) {
7638
+ const file = String(item || '');
7639
+ if (!file) continue;
7640
+ if (!sent.has(file)) {
7641
+ block.dropped.push(file);
7642
+ continue;
7643
+ }
7644
+ if (evicted.indexOf(file) === -1) evicted.push(file);
7645
+ }
7646
+ let selected = null;
7647
+ if (Array.isArray(run.data.selected) && run.data.complete === true && !truncated) {
7648
+ const picked = [];
7649
+ for (const item of run.data.selected) {
7650
+ const file = String(item || '');
7651
+ if (!file) continue;
7652
+ if (!sent.has(file)) {
7653
+ block.dropped.push(file);
7654
+ continue;
7655
+ }
7656
+ if (picked.indexOf(file) === -1) picked.push(file);
7657
+ }
7658
+ selected = new Set(picked);
7659
+ }
7660
+ block.evicted = evicted;
7661
+ block.applied = evicted.length > 0 || selected !== null;
7662
+ if (truncated && Array.isArray(run.data.selected)) {
7663
+ block.note = block.note || '候选超过 ' + MEMORY_HOOK_MAX_CANDIDATES + ',白名单模式未应用(保护)';
7664
+ }
7665
+ return { block: block, evict: new Set(evicted), selected: selected };
7666
+ }
7667
+
7192
7668
  function contextCore(opts) {
7193
7669
  opts = opts || {};
7194
7670
  const roots = memoryRoots();
@@ -7300,9 +7776,26 @@ function contextCore(opts) {
7300
7776
  }
7301
7777
  }
7302
7778
 
7779
+ const memHook = applyMemoryHook(readableEntries, { owner: owner, budget: budget, focus: focus, limit: limit });
7780
+ const hookState = memHook.block;
7781
+ function hookAllows(entry) {
7782
+ if (!entry) return false;
7783
+ if (memHook.selected && !memHook.selected.has(entry.file)) return false;
7784
+ if (memHook.evict.has(entry.file)) return false;
7785
+ return true;
7786
+ }
7787
+ if (hookState.status !== 'not_installed') {
7788
+ const anchor = lines.indexOf('## 1. 身份');
7789
+ lines.splice(anchor === -1 ? 2 : anchor, 0, '- 扩展装载(memory.hook): ' + hookStatusText(hookState), '');
7790
+ trace.push('[hook] status: ' + hookState.status
7791
+ + ' provider: ' + (hookState.provider_id || '-')
7792
+ + ' evicted: ' + hookState.evicted.length
7793
+ + ' dropped: ' + hookState.dropped.length);
7794
+ }
7795
+
7303
7796
  lines.push('## 2.5 长期理解摘要');
7304
7797
  lines.push('');
7305
- const summaries = readableEntries.filter(isConsolidatedSummary).sort(compareContextRecent);
7798
+ const summaries = readableEntries.filter(isConsolidatedSummary).filter(hookAllows).sort(compareContextRecent);
7306
7799
  const summaryLimit = Math.min(3, Math.max(0, limit));
7307
7800
  if (!summaries.length) lines.push('(暂无周期摘要;可用 yotta-memory consolidate --apply 生成)');
7308
7801
  for (const e of summaries.slice(0, summaryLimit)) {
@@ -7325,7 +7818,7 @@ function contextCore(opts) {
7325
7818
  });
7326
7819
  const focusedEntries = focused.entries || [];
7327
7820
  if (!focusedEntries.length) lines.push('(无匹配记忆)');
7328
- for (const e of focusedEntries) {
7821
+ for (const e of focusedEntries.filter(hookAllows)) {
7329
7822
  appendContextEntry(e, 'focus_match score: ' + round3(e.score));
7330
7823
  }
7331
7824
  lines.push('');
@@ -7336,6 +7829,7 @@ function contextCore(opts) {
7336
7829
  const corridor = readableEntries
7337
7830
  .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT'; })
7338
7831
  .sort(compareContextRecent)
7832
+ .filter(hookAllows)
7339
7833
  .filter(function (e) { return !shownFiles.has(e.file); })
7340
7834
  .slice(0, limit);
7341
7835
  if (!corridor.length) lines.push('(暂无近期记忆)');
@@ -7345,7 +7839,7 @@ function contextCore(opts) {
7345
7839
  lines.push('## 4. 近期高价值记忆(补位)');
7346
7840
  lines.push('');
7347
7841
  const highValue = readableEntries
7348
- .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file); })
7842
+ .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file) && hookAllows(e); })
7349
7843
  .map(function (e) { return { e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) }; })
7350
7844
  .sort(function (a, b) { return b.s - a.s; })
7351
7845
  .slice(0, limit);
@@ -7405,12 +7899,16 @@ function contextCore(opts) {
7405
7899
  for (const message of reliability.warnings) lines.push('- [警告] ' + message);
7406
7900
  if (!reliability.ok) lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
7407
7901
  }
7408
- return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace };
7902
+ return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace, hook: hookState };
7409
7903
  }
7410
7904
  function cmdContext(opts) {
7411
7905
  const r = contextCore(opts);
7412
- if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
7413
- else console.log(r.text);
7906
+ if (opts.json) {
7907
+ if (r.report) console.log(JSON.stringify(r.report, null, 2));
7908
+ else console.log(JSON.stringify({ schema: 1, hook: r.hook || null, text: r.text }, null, 2));
7909
+ } else {
7910
+ console.log(r.text);
7911
+ }
7414
7912
  if (r.error) {
7415
7913
  const reminder = migrationReminderText(userRoot());
7416
7914
  if (reminder) console.log('\n' + reminder);
@@ -8724,8 +9222,10 @@ async function cmdScan(opts) {
8724
9222
  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); }
8725
9223
  // v0.17.0 B2:doctor 规模体检阈值键(0 表示只要超过 0 就告警,便于压测与演练)
8726
9224
  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'];
9225
+ // v0.18.1 验收修正:config set / get 共用一份键列表,新增配置键不会再出现「读端认、写端拒」。
9226
+ const CONFIG_VALUE_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', 'maintain_rule_min_hits', '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);
8727
9227
  function cmdConfigSet(key, value) {
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);
9228
+ const known = CONFIG_VALUE_KEYS;
8729
9229
  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); }
8730
9230
  if (value === undefined || value === null || value === '') { console.error('缺少值: config set ' + key + ' <值>'); process.exit(2); }
8731
9231
  const cfg = loadConfig();
@@ -8761,7 +9261,7 @@ function cmdConfigGet(opts) {
8761
9261
  console.log('usage_enabled: ' + (cfg.usage_enabled === undefined ? '(默认 true)' : cfg.usage_enabled));
8762
9262
  console.log('embedding_cmd: ' + (cfg.embedding_cmd || '(未设置)'));
8763
9263
  console.log('embedding_timeout: ' + (cfg.embedding_timeout || 3000));
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);
9264
+ const keys = CONFIG_VALUE_KEYS;
8765
9265
  for (const k of keys) {
8766
9266
  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;
8767
9267
  if (cfg[k] !== undefined) console.log(k + ': ' + cfg[k]);
@@ -8790,7 +9290,7 @@ function mcpTools(profile) {
8790
9290
  { name: 'agent_info', description: '查看当前智能体身份与登记状态(HTTP 读经 token 校验的 X-Agent-Id;stdio 读 --agent-id 显式参数)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
8791
9291
  { name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
8792
9292
  { 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'] } },
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' } } } },
9293
+ { name: 'maintain', description: '记忆自组织(自我进化):规则层归档/遗忘/去重预览。默认 dry-run;apply 才执行,purge 才真删;capacity=true 改为只读容量水位报告;rules=true 改为只读规则晋升建议', inputSchema: { type: 'object', properties: { apply: { type: 'boolean' }, purge: { type: 'boolean' }, dedup: { type: 'boolean' }, capacity: { type: 'boolean' }, rules: { type: 'boolean' }, limit: { type: 'number' }, threshold: { type: 'number' }, age: { type: 'number' } } } },
8794
9294
  { 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' } } } },
8795
9295
  { name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体(MCP 不支持 --model 外部命令)', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' } } } },
8796
9296
  { name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
@@ -8921,7 +9421,7 @@ function callToolInner(name, args, ctx) {
8921
9421
  if (name === 'maintain') {
8922
9422
  const r = args.capacity
8923
9423
  ? 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 });
9424
+ : maintainCore({ selfAgent: agent, apply: !!args.apply, purge: !!args.purge, dedup: !!args.dedup, rules: !!args.rules, threshold: args.threshold, age: args.age });
8925
9425
  return { text: r.text, error: r.error };
8926
9426
  }
8927
9427
  if (name === 'consolidate') {
@@ -9761,6 +10261,14 @@ function consolidateUndoCore(batch, opts) {
9761
10261
  try {
9762
10262
  if (fs.existsSync(to)) fs.unlinkSync(to);
9763
10263
  if (fs.existsSync(from)) {
10264
+ const archiveOwner = archiveOwnerFromPath(x.root, from);
10265
+ try {
10266
+ const marked = readMemoryText(x.root, from, archiveOwner);
10267
+ const clean = stripConsolidateMarker(marked, x.rec.had_trailing_newline !== false);
10268
+ if (clean !== marked) writeMemoryText(x.root, from, clean, archiveOwner);
10269
+ } catch (eMarker) {
10270
+ err = err || ('巩固标记剥离失败: ' + x.rec.file + ' ' + eMarker.message);
10271
+ }
9764
10272
  fs.mkdirSync(path.dirname(to), { recursive: true });
9765
10273
  fs.renameSync(from, to);
9766
10274
  upsertIndexEntry(x.root, readEntry(to, x.root));
@@ -9824,7 +10332,102 @@ function clusterConsolidateCandidates(cands) {
9824
10332
  return groups;
9825
10333
  }
9826
10334
 
9827
- function applyConsolidateGroup(root, group, batch, period) {
10335
+ // ---- v0.18.1 A9:相对日期绝对化 + 归档巩固标记 ----
10336
+ const CONSOLIDATE_MARKER_PREFIX = '<!-- yotta-memory: consolidated to ';
10337
+ const CONSOLIDATE_MARKER_RE = /<!-- yotta-memory: consolidated to [^\n]* -->\n?/;
10338
+ function ymdPad(n) { return String(n).padStart(2, '0'); }
10339
+ function ymdParse(s) {
10340
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || ''));
10341
+ if (!m) return null;
10342
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
10343
+ if (d.getFullYear() !== Number(m[1]) || d.getMonth() !== Number(m[2]) - 1 || d.getDate() !== Number(m[3])) return null;
10344
+ return d;
10345
+ }
10346
+ function ymdFormat(d) { return d.getFullYear() + '-' + ymdPad(d.getMonth() + 1) + '-' + ymdPad(d.getDate()); }
10347
+ function ymdAdd(s, days) { const d = ymdParse(s); if (!d) return ''; d.setDate(d.getDate() + days); return ymdFormat(d); }
10348
+ function ymdWeekRange(s) {
10349
+ const d = ymdParse(s);
10350
+ if (!d) return '';
10351
+ const mondayOffset = (d.getDay() + 6) % 7;
10352
+ const mon = new Date(d.getFullYear(), d.getMonth(), d.getDate() - mondayOffset);
10353
+ const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6);
10354
+ return ymdFormat(mon) + ' ~ ' + ymdFormat(sun);
10355
+ }
10356
+ function ymdMonthRange(s, offset) {
10357
+ const d = ymdParse(s);
10358
+ if (!d) return '';
10359
+ const first = new Date(d.getFullYear(), d.getMonth() + (offset || 0), 1);
10360
+ const last = new Date(first.getFullYear(), first.getMonth() + 1, 0);
10361
+ return ymdFormat(first) + ' ~ ' + ymdFormat(last);
10362
+ }
10363
+ function ymdYearRange(s, offset) {
10364
+ const d = ymdParse(s);
10365
+ if (!d) return '';
10366
+ const y = d.getFullYear() + (offset || 0);
10367
+ return y + '-01-01 ~ ' + y + '-12-31';
10368
+ }
10369
+ const RELATIVE_DATE_MAP = {
10370
+ '大前天': function (b) { return ymdAdd(b, -3); },
10371
+ '上上周': function (b) { return ymdWeekRange(ymdAdd(b, -7)); },
10372
+ '上个月': function (b) { return ymdMonthRange(b, -1); },
10373
+ '这个月': function (b) { return ymdMonthRange(b, 0); },
10374
+ '前天': function (b) { return ymdAdd(b, -2); },
10375
+ '昨天': function (b) { return ymdAdd(b, -1); },
10376
+ '昨日': function (b) { return ymdAdd(b, -1); },
10377
+ '今天': function (b) { return ymdAdd(b, 0); },
10378
+ '今日': function (b) { return ymdAdd(b, 0); },
10379
+ '明天': function (b) { return ymdAdd(b, 1); },
10380
+ '后天': function (b) { return ymdAdd(b, 2); },
10381
+ '上周': function (b) { return ymdWeekRange(ymdAdd(b, -7)); },
10382
+ '本周': function (b) { return ymdWeekRange(b); },
10383
+ '这周': function (b) { return ymdWeekRange(b); },
10384
+ '上月': function (b) { return ymdMonthRange(b, -1); },
10385
+ '本月': function (b) { return ymdMonthRange(b, 0); },
10386
+ '今年': function (b) { return ymdYearRange(b, 0); },
10387
+ '去年': function (b) { return ymdYearRange(b, -1); },
10388
+ };
10389
+ const RELATIVE_DATE_RE = /(大前天|上上周|上个月|这个月|前天|昨天|昨日|今天|今日|明天|后天|上周|本周|这周|上月|本月|今年|去年)(?![((]\d{4}-\d{2}-\d{2})/g;
10390
+ function absolutizeRelativeDates(text, baseYmd) {
10391
+ const s = String(text || '');
10392
+ if (!ymdParse(baseYmd)) return { text: s, count: 0 };
10393
+ let count = 0;
10394
+ const out = s.replace(RELATIVE_DATE_RE, function (word) {
10395
+ const fn = RELATIVE_DATE_MAP[word];
10396
+ if (!fn) return word;
10397
+ const rep = fn(baseYmd);
10398
+ if (!rep) return word;
10399
+ count += 1;
10400
+ return word + '(' + rep + ')';
10401
+ });
10402
+ return { text: out, count: count };
10403
+ }
10404
+ function consolidateMarkerText(summaryRel, batch) {
10405
+ return CONSOLIDATE_MARKER_PREFIX + summaryRel + ' | batch ' + batch + ' | ' + today() + ' -->';
10406
+ }
10407
+ function archiveOwnerFromPath(root, fp) {
10408
+ const seg = path.relative(root, fp).split(/[\\/]/);
10409
+ if (seg[0] === ARCHIVE_DIR && seg[1] === PRIVATE_DIR && seg[2]) return seg[2];
10410
+ return ownerFromPrivatePath(root, fp);
10411
+ }
10412
+ function appendConsolidateMarker(root, fp, summaryRel, batch, owner, opts) {
10413
+ if (opts && opts.markerFailForTest) throw new Error('marker fail for test');
10414
+ let text = readMemoryText(root, fp, owner);
10415
+ if (text.indexOf(CONSOLIDATE_MARKER_PREFIX) !== -1) return { appended: false, hadTrailingNewline: text.endsWith('\n') };
10416
+ const hadTrailingNewline = text.endsWith('\n');
10417
+ if (!hadTrailingNewline) text += '\n';
10418
+ text += consolidateMarkerText(summaryRel, batch) + '\n';
10419
+ writeMemoryText(root, fp, text, owner);
10420
+ return { appended: true, hadTrailingNewline: hadTrailingNewline };
10421
+ }
10422
+ function stripConsolidateMarker(text, hadTrailingNewline) {
10423
+ let out = String(text || '');
10424
+ if (out.indexOf(CONSOLIDATE_MARKER_PREFIX) === -1) return out;
10425
+ out = out.replace(CONSOLIDATE_MARKER_RE, '');
10426
+ if (hadTrailingNewline === false && out.endsWith('\n')) out = out.slice(0, -1);
10427
+ return out;
10428
+ }
10429
+
10430
+ function applyConsolidateGroup(root, group, batch, period, opts) {
9828
10431
  const e0 = group[0];
9829
10432
  const type = e0.type;
9830
10433
  const owner = e0.scope === 'private' ? e0.owner : '';
@@ -9848,7 +10451,12 @@ function applyConsolidateGroup(root, group, batch, period) {
9848
10451
  const avgU = round3(group.reduce(function (s, c) { return s + c.utility; }, 0) / group.length);
9849
10452
  const bodyLines = ['# 周期摘要:' + theme, '', '> 生成: yotta-memory consolidate | 类型 ' + type + ' | 范围 ' + createdDates[0] + ' ~ ' + createdDates[createdDates.length - 1] + ' | ' + group.length + ' 条 | 平均效用 ' + avgU, '', '## 主题要点', ''];
9850
10453
  const sorted = group.slice().sort(function (a, b) { return String(b.e.updated || '').localeCompare(String(a.e.updated || '')); });
9851
- for (const c of sorted) bodyLines.push('- [' + c.e.type + '] ' + (c.e.subject || '') + ': ' + (c.e.statement || ''));
10454
+ let dateNormalized = 0;
10455
+ for (const c of sorted) {
10456
+ const norm = absolutizeRelativeDates(c.e.statement || '', c.e.created || c.e.updated || '');
10457
+ dateNormalized += norm.count;
10458
+ bodyLines.push('- [' + c.e.type + '] ' + (c.e.subject || '') + ': ' + norm.text);
10459
+ }
9852
10460
  bodyLines.push('', '## 溯源(原文已入 .archive,可 consolidate --undo <batch> 恢复)', '');
9853
10461
  for (const c of group) bodyLines.push('- ' + c.e.file + '(created ' + c.e.created + ' / confidence ' + c.e.confidence + ' / utility ' + round3(c.utility) + ')');
9854
10462
  const body = bodyLines.join('\n');
@@ -9859,16 +10467,26 @@ function applyConsolidateGroup(root, group, batch, period) {
9859
10467
  upsertIndexEntry(root, readEntry(fp, root));
9860
10468
  appendAudit(root, 'audit', { action: 'summary_create', ts: new Date().toISOString(), batch: batch, file: summaryRel, sourceFiles: group.map(function (c) { return c.e.file; }) });
9861
10469
  let moved = 0;
10470
+ let markersWritten = 0;
10471
+ let markerErrors = 0;
9862
10472
  for (const c of group) {
9863
10473
  const destDir = archiveDirFor(root, type, owner);
9864
10474
  fs.mkdirSync(destDir, { recursive: true });
9865
10475
  const dest = path.join(destDir, path.basename(c.e.file));
9866
10476
  fs.renameSync(path.join(root, c.e.file), dest);
10477
+ let marker = { appended: false, hadTrailingNewline: true };
10478
+ try {
10479
+ marker = appendConsolidateMarker(root, dest, summaryRel, batch, owner, opts);
10480
+ if (marker.appended) markersWritten += 1;
10481
+ } catch (e) {
10482
+ markerErrors += 1;
10483
+ appendAudit(root, 'audit', { action: 'marker_failed', ts: new Date().toISOString(), batch: batch, file: c.e.file, to: relOf(root, dest), error: String(e.message || e) });
10484
+ }
9867
10485
  removeIndexEntry(root, c.e.file);
9868
- appendAudit(root, 'audit', { action: 'archive', ts: new Date().toISOString(), batch: batch, file: c.e.file, to: relOf(root, dest), type: type, owner: owner, utility: round3(c.utility) });
10486
+ appendAudit(root, 'audit', { action: 'archive', ts: new Date().toISOString(), batch: batch, file: c.e.file, to: relOf(root, dest), type: type, owner: owner, utility: round3(c.utility), had_trailing_newline: marker.hadTrailingNewline === true });
9869
10487
  moved++;
9870
10488
  }
9871
- return { made: 1, moved: moved, summaryRel: summaryRel };
10489
+ return { made: 1, moved: moved, summaryRel: summaryRel, dateNormalized: dateNormalized, markersWritten: markersWritten, markerErrors: markerErrors };
9872
10490
  }
9873
10491
 
9874
10492
  // v0.18.0 A5:首次启用说明按 .archive 审计判定(不新建标记文件)。
@@ -9969,9 +10587,12 @@ function consolidateCore(opts) {
9969
10587
  summary_subject: '周期摘要 ' + theme + '(' + period + ' 天窗)',
9970
10588
  files: g.map(function (c) { return c.e.file; }),
9971
10589
  archive_to: path.relative(root, archiveDirFor(root, type, owner)).replace(/\\/g, '/'),
10590
+ date_normalized_preview: g.reduce(function (s, c) {
10591
+ return s + absolutizeRelativeDates(c.e.statement || '', c.e.created || c.e.updated || '').count;
10592
+ }, 0),
9972
10593
  };
9973
10594
  };
9974
- const makeConsolidateReport = function (builtGroups, batch) {
10595
+ const makeConsolidateReport = function (builtGroups, batch, extras) {
9975
10596
  return {
9976
10597
  schemaVersion: 1,
9977
10598
  mode: apply ? 'apply' : 'propose',
@@ -9987,6 +10608,9 @@ function consolidateCore(opts) {
9987
10608
  stats: stats,
9988
10609
  candidates: cands.length,
9989
10610
  groups: builtGroups.map(makeConsolidateGroup),
10611
+ date_normalized: (extras && extras.dateNormalized) || 0,
10612
+ markers_written: (extras && extras.markersWritten) || 0,
10613
+ marker_errors: (extras && extras.markerErrors) || 0,
9990
10614
  retention: '原文移入 .archive/ 后一直保留,直到显式 purge 或 undo。',
9991
10615
  rollback: batch
9992
10616
  ? ('yotta-memory consolidate --undo ' + batch)
@@ -10015,7 +10639,10 @@ function consolidateCore(opts) {
10015
10639
  const minA = Math.min.apply(null, ages), maxA = Math.max.apply(null, ages);
10016
10640
  const avgU = round3(g.reduce(function (s, c) { return s + c.utility; }, 0) / g.length);
10017
10641
  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, '/'));
10642
+ const datePreview = g.reduce(function (s, c) {
10643
+ return s + absolutizeRelativeDates(c.e.statement || '', c.e.created || c.e.updated || '').count;
10644
+ }, 0);
10645
+ lines.push(' → 将生成 周期摘要 ' + pickSummaryTheme(g) + '(' + period + ' 天窗),日期归一预览 ' + datePreview + ' 处,原文归档到 ' + path.relative(root, archiveDirFor(root, type, g[0].owner || '')).replace(/\\/g, '/'));
10019
10646
  for (const c of g) lines.push(' · ' + c.e.file + ' — ' + String(c.e.subject || '').slice(0, 32) + ': ' + String(c.e.statement || '').slice(0, 60));
10020
10647
  }
10021
10648
  if (!apply) {
@@ -10041,16 +10668,18 @@ function consolidateCore(opts) {
10041
10668
  lines.push('');
10042
10669
  lines.push('- 事务快照: ' + guard.snapshot.id);
10043
10670
  lines.push('- 批次: ' + batch + '(审计 .archive/audit-<日期>.jsonl;回滚: yotta-memory consolidate --undo ' + batch + ')');
10044
- let made = 0, moved = 0;
10671
+ let made = 0, moved = 0, dateNormalized = 0, markersWritten = 0, markerErrors = 0;
10045
10672
  for (const g of groups) {
10046
- const res = applyConsolidateGroup(root, g, batch, period);
10673
+ const res = applyConsolidateGroup(root, g, batch, period, opts);
10047
10674
  made += res.made; moved += res.moved;
10675
+ dateNormalized += res.dateNormalized; markersWritten += res.markersWritten; markerErrors += res.markerErrors;
10048
10676
  lines.push('- 已生成摘要 ' + res.summaryRel + '(归档原文 ' + res.moved + ' 条)');
10049
10677
  }
10050
10678
  appendAudit(root, 'audit', { action: 'batch_done', ts: new Date().toISOString(), batch: batch, command: 'consolidate', summaries: made, archived: moved, active_after: collectEntryFiles(root).length });
10051
10679
  lines.push('- 完成: 生成摘要 ' + made + ' 条 / 原文归档 ' + moved + ' 条');
10680
+ lines.push('- 日期归一: ' + dateNormalized + ' 处;巩固标记: ' + markersWritten + ' 条(失败 ' + markerErrors + ')');
10052
10681
  if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
10053
- return { error: false, text: lines.join('\n'), report: makeConsolidateReport(groups, batch) };
10682
+ return { error: false, text: lines.join('\n'), report: makeConsolidateReport(groups, batch, { dateNormalized: dateNormalized, markersWritten: markersWritten, markerErrors: markerErrors }) };
10054
10683
  }
10055
10684
 
10056
10685
  async function cmdConsolidate(opts) {
@@ -10372,6 +11001,7 @@ async function main() {
10372
11001
  else if (a === '--keep-identity') opts.keepIdentity = true;
10373
11002
  else if (a === '--no-usage') opts.noUsage = true;
10374
11003
  else if (a === '--capacity') opts.capacity = true;
11004
+ else if (a === '--rules') opts.rules = true;
10375
11005
  else if (a === '--propose') opts.propose = true;
10376
11006
  else if (a === '--audit') opts.audit = true;
10377
11007
  else if (valueOpts.has(a)) {
@@ -10574,6 +11204,7 @@ module.exports = {
10574
11204
  profileCore: profileCore,
10575
11205
  contextCore: contextCore,
10576
11206
  contextAuditCore: contextAuditCore,
11207
+ applyMemoryHook: applyMemoryHook,
10577
11208
  importanceScore: importanceScore,
10578
11209
  usageSettings: usageSettings,
10579
11210
  usageTotals: usageTotals,