@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.
- package/CHANGELOG.md +35 -0
- package/README.md +1 -0
- package/README.zh-CN.md +3 -0
- package/SKILL.md +26 -5
- package/USER_GUIDE.md +19 -5
- package/bin/provider.js +248 -0
- package/bin/yotta-memory.js +662 -31
- package/package.json +1 -1
- package/references/faq.md +9 -0
- package/references/protocol.md +31 -2
- package/references/provider-protocol.md +100 -0
- package/skill-manifest.json +2 -2
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.
|
|
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
|
-
//
|
|
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('# 元忆记忆蒸馏报告(
|
|
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('##
|
|
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('##
|
|
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##
|
|
5841
|
+
body += '\n## 七、模型提炼(--model ' + opts.model + ')\n\n' + modelOut + '\n';
|
|
5482
5842
|
} catch (e) {
|
|
5483
5843
|
modelOut = '(模型调用失败: ' + e.message + ')';
|
|
5484
|
-
body += '\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.
|
|
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
|
|
7413
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|