@yottameta/yotta-memory 0.17.4 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +3 -1
- package/README.zh-CN.md +10 -6
- package/SKILL.md +33 -8
- package/USER_GUIDE.md +30 -7
- package/bin/yotta-memory.js +1445 -90
- package/package.json +1 -1
- package/references/faq.md +18 -0
- package/references/protocol.md +57 -3
- package/skill-manifest.json +1 -1
package/bin/yotta-memory.js
CHANGED
|
@@ -25,9 +25,9 @@ const net = require('net');
|
|
|
25
25
|
const child_process = require('child_process');
|
|
26
26
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
27
27
|
|
|
28
|
-
const VERSION = '0.
|
|
29
|
-
const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path']);
|
|
30
|
-
const CLI_FLAG_OPTS = new Set(['--project', '--all', '--unsafe', '--no-auth', '--stdio', '--onstart', '--from-current', '--restart', '--force', '--attach', '--allow-same-volume', '--verify', '--no-hint', '--encrypt', '--no-encrypt', '--password-stdin', '--json', '--manual', '--skip-schedule', '--useful', '--useless', '--undo', '--dry-run', '--apply', '--purge', '--dedup', '--batches', '--explain', '--semantic', '--runtime', '--ablate', '--timing', '--baseline', '--probe', '--quarantine', '--restore', '--yes', '--keep-memories', '--keep-identity']);
|
|
28
|
+
const VERSION = '0.18.1';
|
|
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', '--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 || '' };
|
|
@@ -65,6 +65,7 @@ const HELP_MODEL = [
|
|
|
65
65
|
helpOption('--embedding', '<命令>', '临时指定本地 embedding 插件命令', '这次检索想使用自定义本地向量命令时', '只在本地执行,不会把记忆上传远端'),
|
|
66
66
|
helpOption('--embedding-timeout', '<毫秒>', '设置 embedding 插件超时', '插件较慢或需要快速失败时', ''),
|
|
67
67
|
helpOption('--year', '<yyyy>', '只检索指定年份的记忆', '库很大、只想在某一年的记忆里找时', '可重复传多次;不传则检索全部年份'),
|
|
68
|
+
helpOption('--no-usage', '', '本次不记录命中打点', '只想临时关闭使用统计时', '只影响这一次调用,不改配置'),
|
|
68
69
|
] },
|
|
69
70
|
{ name: 'forget', usage: 'forget <记忆 id> [选项]', what: '删除一条记忆', when: '确认某条记忆不应继续保留时', options: [
|
|
70
71
|
helpOption('--reason', '<原因>', '记录删除原因', '需要留下为什么删除的审计线索时', ''),
|
|
@@ -78,7 +79,8 @@ const HELP_MODEL = [
|
|
|
78
79
|
{ name: 'archive', usage: 'archive [选项]', what: '把低价值或过期记忆归档', when: '库变大,想让旧记忆退出主检索但保留可回溯时', options: [
|
|
79
80
|
helpOption('--days', '<天数>', '按未使用天数判断归档', '想调整「多久没用才归档」时', ''),
|
|
80
81
|
helpOption('--threshold', '<数值>', '按效用分阈值判断归档', '想调整「用得少不少」时', ''),
|
|
81
|
-
helpOption('--dry-run', '', '只预览归档结果', '先看会动哪些记忆,再决定是否执行时', ''),
|
|
82
|
+
helpOption('--dry-run', '', '只预览归档结果', '先看会动哪些记忆,再决定是否执行时', '预演只读,不改任何文件'),
|
|
83
|
+
helpOption('--force', '', '关闭冷却期与标签常青豁免', '确认新条目或 evergreen / pinned 条目也要归档时', 'immutable 与 BOUND 仍然豁免;先确认清单'),
|
|
82
84
|
] },
|
|
83
85
|
{ name: 'backup', usage: 'backup <子命令> [选项]', what: '备份、恢复和演练记忆库', when: '配置自动备份、创建备份、恢复误删或验证备份可用时', subcommands: [
|
|
84
86
|
helpSub('volumes', 'volumes [--json]', '列出可用备份卷', '想知道备份会写到哪里时', [helpOption('--json', '', '输出 JSON', '脚本处理备份卷列表时', '')]),
|
|
@@ -115,6 +117,8 @@ const HELP_MODEL = [
|
|
|
115
117
|
helpOption('--json', '', '输出 JSON', '脚本或自动化读取体检结果时', ''),
|
|
116
118
|
] },
|
|
117
119
|
{ name: 'maintain', usage: 'maintain [选项]', what: '记忆自组织:归档、遗忘候选、去重和合并', when: '定期清理低价值记忆、合并重复条目时', options: [
|
|
120
|
+
helpOption('--capacity', '', '查看容量水位与淘汰 / 晋升建议', '库变大,想看哪些条目值得清理或提升时', '只读报告,不会自动归档或改权重'),
|
|
121
|
+
helpOption('--rules', '', '查看重复踩坑 → 建议升级为规则的清单', '同一类问题反复出现、想把它固化成 BOUND 规则时', '只读报告,不自动写规则;阈值走 config maintain_rule_min_hits'),
|
|
118
122
|
helpOption('--dedup', '', '只做重复检查并给置信度', '想先看哪些条目重复时', ''),
|
|
119
123
|
helpOption('--apply', '', '真正执行,而不是只预览', '确认预览结果后要落盘时', '会改动记忆库并创建事务快照;先跑 dry-run'),
|
|
120
124
|
helpOption('--purge', '', '对遗忘候选执行真删', '确认候选确实不再需要时', '真删不可逆;通常先用归档而不是 purge'),
|
|
@@ -126,8 +130,10 @@ const HELP_MODEL = [
|
|
|
126
130
|
helpOption('--merge', '<id列表>', '显式合并指定条目', '已经确认这些条目可以合并时', ''),
|
|
127
131
|
helpOption('--json', '', '输出 JSON', '自动化处理维护结果时', ''),
|
|
128
132
|
] },
|
|
129
|
-
{ name: 'consolidate', usage: 'consolidate [--apply | --undo <batch> | --batches] [选项]', what: '把旧记忆压缩成周期摘要', when: '库很大,想把超龄、闲置、低效用条目压成摘要时', options: [
|
|
130
|
-
helpOption('--
|
|
133
|
+
{ name: 'consolidate', usage: 'consolidate [--propose | --apply | --undo <batch> | --batches] [选项]', what: '把旧记忆压缩成周期摘要', when: '库很大,想把超龄、闲置、低效用条目压成摘要时', options: [
|
|
134
|
+
helpOption('--propose', '', '只输出结构化候选报告(默认行为)', '想先审阅会压缩哪些内容、保留与回滚口径时', '不写盘;等价于直接运行 consolidate'),
|
|
135
|
+
helpOption('--apply', '', '真正执行压缩', '确认 propose 报告后要落盘时', '会改动记忆库并创建事务快照;先预览'),
|
|
136
|
+
helpOption('--yes', '', '非交互确认执行', '脚本或自动化环境里已经审阅过 propose 报告时', '跳过交互确认;破坏性操作,务必先看报告'),
|
|
131
137
|
helpOption('--undo', '<batch>', '回滚一个摘要批次', '压缩后发现结果不合适时', ''),
|
|
132
138
|
helpOption('--batches', '', '列出历史批次', '想找回滚批次号时', ''),
|
|
133
139
|
helpOption('--period', '<天数>', '设置摘要周期', '想改变摘要覆盖的时间跨度时', ''),
|
|
@@ -141,6 +147,8 @@ const HELP_MODEL = [
|
|
|
141
147
|
{ name: 'distill', usage: 'distill [--model <命令>] [选项]', what: '把心理日志蒸馏成统计摘要、主题画像和知识地图', when: '想把长期记录整理成更高层的自我画像时', options: [
|
|
142
148
|
helpOption('--model', '<命令>', '调用本地外部模型辅助蒸馏', '需要更高质量摘要、且已准备本地模型命令时', '命令在本地执行;不要指向会外传原文的服务'),
|
|
143
149
|
helpOption('--subject', '<标题>', '指定蒸馏输出主题', '想给这次摘要一个固定标题时', ''),
|
|
150
|
+
helpOption('--owner', '<id>', '指定蒸馏归属的智能体', '想蒸馏某个 owner 的记忆时', '默认当前身份;跨 owner 私密仍按 owner 门跳过'),
|
|
151
|
+
helpOption('--out', '<路径>', '把蒸馏报告导出到指定路径', '想把报告放到自定义位置时', '不指定时按归属写入 distills 目录'),
|
|
144
152
|
helpOption('--json', '', '输出 JSON', '自动化处理蒸馏结果时', ''),
|
|
145
153
|
] },
|
|
146
154
|
{ name: 'feedback', usage: 'feedback <记忆 id> [--useful | --useless | --undo]', what: '记录记忆是否有用', when: '一条记忆刚刚帮助了你,或明显误导了你时', options: [
|
|
@@ -148,7 +156,10 @@ const HELP_MODEL = [
|
|
|
148
156
|
helpOption('--useless', '', '记为没用', '这条记忆造成误导或噪音时', ''),
|
|
149
157
|
helpOption('--undo', '', '撤销上一次反馈', '误点反馈时', ''),
|
|
150
158
|
] },
|
|
151
|
-
{ name: 'explain', usage: 'explain <记忆 id> [--json]', what: '解释一条记忆的效用分和归档判定', when: '想理解为什么它被排序、归档或遗忘时', options: [
|
|
159
|
+
{ name: 'explain', usage: 'explain <记忆 id> [--json] [--no-usage]', what: '解释一条记忆的效用分和归档判定', when: '想理解为什么它被排序、归档或遗忘时', options: [
|
|
160
|
+
helpOption('--json', '', '输出 JSON', '脚本读取解释结果时', ''),
|
|
161
|
+
helpOption('--no-usage', '', '本次不记录命中打点', '只想临时关闭使用统计时', '只影响这一次调用,不改配置'),
|
|
162
|
+
] },
|
|
152
163
|
{ name: 'bench', usage: 'bench [--evalset <文件>] [--k <条数>] [--seed <数值>] [--bootstrap <次数>] [--ablate] [--gate <指标>=<数值>] [--timing] [--json] [--out <文件>]', what: '可复算的检索基准评测', when: '改检索 / 索引逻辑前后要证明「没变差」,或把质量门禁接进 CI 时', options: [
|
|
153
164
|
helpOption('--evalset', '<文件>', '指定评测集 JSON 文件', '想固定一组查询来复算指标时', '不指定时按库内条目做确定性抽样,生成基线评测集'),
|
|
154
165
|
helpOption('--k', '<条数>', '设置结果窗口大小', '想按 Recall@k / nDCG@k 的 k 口径统计时', '默认 5'),
|
|
@@ -195,7 +206,9 @@ const HELP_MODEL = [
|
|
|
195
206
|
helpOption('--owner', '<id>', '指定画像归属者', '查某个 owner 的私密画像时', '仍然受私密区权限约束'),
|
|
196
207
|
helpOption('--json', '', '输出 JSON', '自动化读取画像时', ''),
|
|
197
208
|
] },
|
|
198
|
-
{ name: 'context', usage: 'context [选项]', what: '生成开工上下文包', when: '每个会话开工时恢复身份、边界、近期记忆和承诺', options: [
|
|
209
|
+
{ name: 'context', usage: 'context [--audit [--from <文件|->]] [选项]', what: '生成开工上下文包', when: '每个会话开工时恢复身份、边界、近期记忆和承诺', options: [
|
|
210
|
+
helpOption('--audit', '', '审计被压缩掉的内容是否已经落盘', '宿主压缩上下文后,想确认有没有决策只存在于对话里时', '只读:只打印 remember 建议,不会自动补写'),
|
|
211
|
+
helpOption('--from', '<文件|->', '指定待审计内容(- 表示标准输入)', '把宿主压缩摘要或丢弃段落喂给审计时', '不传时审计当前上下文包的丢弃清单'),
|
|
199
212
|
helpOption('--limit', '<条数>', '限制近期记忆条数', '上下文太长、想缩短时', ''),
|
|
200
213
|
helpOption('--owner', '<id>', '指定 owner 范围', '需要看某个 owner 的上下文时', '仍然受私密区权限约束'),
|
|
201
214
|
helpOption('--budget', '<字符数>', '设置动态记忆字符预算', '需要控制上下文包大小时', '0 表示不限制'),
|
|
@@ -204,6 +217,7 @@ const HELP_MODEL = [
|
|
|
204
217
|
helpOption('--embedding', '<命令>', '临时指定本地 embedding 插件命令', '上下文检索想用本地向量命令时', '只在本地执行'),
|
|
205
218
|
helpOption('--embedding-timeout', '<毫秒>', '设置 embedding 插件超时', '插件较慢或需要快速失败时', ''),
|
|
206
219
|
helpOption('--year', '<yyyy>', '只装载指定年份的记忆', '只想让上下文包覆盖某一年份时', '可重复传多次;不传则装载全部年份'),
|
|
220
|
+
helpOption('--no-usage', '', '本次不记录命中打点', '只想临时关闭使用统计时', '只影响这一次调用,不改配置'),
|
|
207
221
|
] },
|
|
208
222
|
{ name: 'token', usage: 'token <子命令> [选项]', what: '管理远端 MCP 访问 token', when: '给远端 MCP 客户端发放或吊销访问凭据时', subcommands: [
|
|
209
223
|
helpSub('new', 'new --agent <id> [--scope <范围>] [--json]', '生成一个访问 token', '远端客户端第一次接入时', [
|
|
@@ -304,7 +318,7 @@ const PRIVATE_LEAF = ['prefs', 'bounds', 'commits'];
|
|
|
304
318
|
const ARCHIVE_DIR = '.archive';
|
|
305
319
|
const INDEX_FILE = 'index.json';
|
|
306
320
|
const PRIVATE_TYPES = ['PREF', 'BOUND', 'COMMIT'];
|
|
307
|
-
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed', 'feedback_net'];
|
|
321
|
+
const FIELD_ORDER = ['type', 'subject', 'statement', 'confidence', 'created', 'updated', 'tags', 'immutable', 'scope', 'owner', 'source', 'weight', 'access_count', 'last_accessed', 'feedback_net', 'hit_days', 'hit_queries'];
|
|
308
322
|
const CONFIG_FILE = 'config.json';
|
|
309
323
|
const SERVER_SUBDIR = '.server';
|
|
310
324
|
const TOKENS_FILE = 'tokens.json';
|
|
@@ -1330,6 +1344,129 @@ function semanticMatch(entry, query, wantDetail) {
|
|
|
1330
1344
|
function round3(n) { return Math.round(n * 1000) / 1000; }
|
|
1331
1345
|
// ---- v0.8.0 自我进化:统一效用分(盖棺分)----
|
|
1332
1346
|
function feedbackNetOf(meta) { return parseInt(meta.feedback_net || '0', 10) || 0; }
|
|
1347
|
+
// ---- v0.18.0 A3:命中打点(本地元数据;查询只存 8 位指纹,不落原文)----
|
|
1348
|
+
// hit_days 形如 s2026-09-26:2|g2026-09-25:1|u2026-09-24:1(s=search / g=get / u=use)。
|
|
1349
|
+
// hit_queries 形如 q3f9a1c7:2|q77bd021e:1(sha256(query) 前 8 位十六进制 + 次数)。
|
|
1350
|
+
const USAGE_SIGNAL_LABELS = { search: 's', get: 'g', use: 'u' };
|
|
1351
|
+
const USAGE_SIGNAL_ORDER = { s: 0, g: 1, u: 2 };
|
|
1352
|
+
const USAGE_CONFIG_DEFAULTS = { usage_retention_days: 90, usage_query_slots: 12 };
|
|
1353
|
+
function usageNumber(cfg, key) {
|
|
1354
|
+
const raw = cfg ? cfg[key] : undefined;
|
|
1355
|
+
if (raw === undefined || raw === null || raw === '') return USAGE_CONFIG_DEFAULTS[key];
|
|
1356
|
+
const n = parseFloat(raw);
|
|
1357
|
+
return (n >= 0) ? n : USAGE_CONFIG_DEFAULTS[key];
|
|
1358
|
+
}
|
|
1359
|
+
function usageSettings(opts) {
|
|
1360
|
+
const cfg = loadConfig();
|
|
1361
|
+
return {
|
|
1362
|
+
enabled: !(opts && opts.noUsage) && cfg.usage_enabled !== false,
|
|
1363
|
+
retentionDays: usageNumber(cfg, 'usage_retention_days'),
|
|
1364
|
+
querySlots: usageNumber(cfg, 'usage_query_slots'),
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function usageFingerprint(query) {
|
|
1368
|
+
return crypto.createHash('sha256').update(String(query), 'utf8').digest('hex').slice(0, 8);
|
|
1369
|
+
}
|
|
1370
|
+
function parseHitDays(value) {
|
|
1371
|
+
const map = {};
|
|
1372
|
+
const re = /([sgu])(\d{4}-\d{2}-\d{2}):(\d+)/g;
|
|
1373
|
+
let m;
|
|
1374
|
+
while ((m = re.exec(String(value || '')))) {
|
|
1375
|
+
const key = m[1] + m[2];
|
|
1376
|
+
map[key] = (map[key] || 0) + (parseInt(m[3], 10) || 0);
|
|
1377
|
+
}
|
|
1378
|
+
return map;
|
|
1379
|
+
}
|
|
1380
|
+
function serializeHitDays(map) {
|
|
1381
|
+
const keys = Object.keys(map).filter(function (k) { return /^[sgu]\d{4}-\d{2}-\d{2}$/.test(k) && map[k] > 0; });
|
|
1382
|
+
keys.sort(function (a, b) {
|
|
1383
|
+
const da = a.slice(1), db = b.slice(1);
|
|
1384
|
+
if (da !== db) return db.localeCompare(da);
|
|
1385
|
+
return (USAGE_SIGNAL_ORDER[a[0]] || 0) - (USAGE_SIGNAL_ORDER[b[0]] || 0);
|
|
1386
|
+
});
|
|
1387
|
+
return keys.map(function (k) { return k + ':' + map[k]; }).join('|');
|
|
1388
|
+
}
|
|
1389
|
+
function parseHitQueries(value) {
|
|
1390
|
+
const map = {};
|
|
1391
|
+
const re = /q([0-9a-f]{8}):(\d+)/g;
|
|
1392
|
+
let m;
|
|
1393
|
+
while ((m = re.exec(String(value || '')))) {
|
|
1394
|
+
map[m[1]] = (map[m[1]] || 0) + (parseInt(m[2], 10) || 0);
|
|
1395
|
+
}
|
|
1396
|
+
return map;
|
|
1397
|
+
}
|
|
1398
|
+
function serializeHitQueries(map, slots) {
|
|
1399
|
+
const fps = Object.keys(map).filter(function (fp) { return /^[0-9a-f]{8}$/.test(fp) && map[fp] > 0; });
|
|
1400
|
+
fps.sort(function (a, b) {
|
|
1401
|
+
if (map[b] !== map[a]) return map[b] - map[a];
|
|
1402
|
+
return a.localeCompare(b);
|
|
1403
|
+
});
|
|
1404
|
+
const limit = (slots >= 0) ? slots : USAGE_CONFIG_DEFAULTS.usage_query_slots;
|
|
1405
|
+
return fps.slice(0, limit).map(function (fp) { return 'q' + fp + ':' + map[fp]; }).join('|');
|
|
1406
|
+
}
|
|
1407
|
+
// 纯函数:算好本次命中要写入的 frontmatter 字段(不写盘,便于 recall/feedback 复用同一口径)。
|
|
1408
|
+
function usagePatchForMeta(meta, signal, query, settings, dateStr) {
|
|
1409
|
+
const label = USAGE_SIGNAL_LABELS[signal];
|
|
1410
|
+
if (!label) return {};
|
|
1411
|
+
const patch = {};
|
|
1412
|
+
const days = parseHitDays(meta.hit_days);
|
|
1413
|
+
const key = label + dateStr;
|
|
1414
|
+
days[key] = (days[key] || 0) + 1;
|
|
1415
|
+
const kept = {};
|
|
1416
|
+
for (const k of Object.keys(days)) {
|
|
1417
|
+
const age = daysBetween(k.slice(1), dateStr);
|
|
1418
|
+
if (age < 0 || age > settings.retentionDays) continue;
|
|
1419
|
+
kept[k] = days[k];
|
|
1420
|
+
}
|
|
1421
|
+
const daysStr = serializeHitDays(kept);
|
|
1422
|
+
if (daysStr) patch.hit_days = daysStr;
|
|
1423
|
+
if (signal === 'search' && query) {
|
|
1424
|
+
const queries = parseHitQueries(meta.hit_queries);
|
|
1425
|
+
const fp = usageFingerprint(query);
|
|
1426
|
+
queries[fp] = (queries[fp] || 0) + 1;
|
|
1427
|
+
patch.hit_queries = serializeHitQueries(queries, settings.querySlots);
|
|
1428
|
+
}
|
|
1429
|
+
return patch;
|
|
1430
|
+
}
|
|
1431
|
+
// 独立写点(explain / context use 信号):写失败只计数,不影响读取结果。
|
|
1432
|
+
function applyUsageHits(root, relFiles, signal, query, opts) {
|
|
1433
|
+
opts = opts || {};
|
|
1434
|
+
const out = { enabled: false, written: 0, skipped: 0, failed: 0 };
|
|
1435
|
+
const settings = usageSettings(opts);
|
|
1436
|
+
out.enabled = settings.enabled;
|
|
1437
|
+
if (!settings.enabled) return out;
|
|
1438
|
+
const selfAgent = (opts.selfAgent !== undefined) ? opts.selfAgent : resolveIdentity(opts).id;
|
|
1439
|
+
const dateStr = today();
|
|
1440
|
+
for (const rel of relFiles) {
|
|
1441
|
+
const fp = path.join(root, rel);
|
|
1442
|
+
if (!fs.existsSync(fp)) continue;
|
|
1443
|
+
const owner = ownerFromPrivatePath(root, fp);
|
|
1444
|
+
if (owner && owner !== selfAgent) { out.skipped++; continue; }
|
|
1445
|
+
try {
|
|
1446
|
+
const parsed = parseFrontmatter(readMemoryText(root, fp, owner));
|
|
1447
|
+
if (parsed.meta.immutable === 'true') { out.skipped++; continue; }
|
|
1448
|
+
const patch = usagePatchForMeta(parsed.meta, signal, query, settings, dateStr);
|
|
1449
|
+
if (!Object.keys(patch).length) continue;
|
|
1450
|
+
const meta = Object.assign({}, parsed.meta, patch);
|
|
1451
|
+
writeMemoryText(root, fp, frontmatterToText(meta, parsed.body), owner);
|
|
1452
|
+
upsertIndexEntry(root, readEntry(fp, root));
|
|
1453
|
+
out.written++;
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
out.failed++;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return out;
|
|
1459
|
+
}
|
|
1460
|
+
// 报告侧派生:窗口内命中次数(不落盘,避免派生值与原始值漂移)。
|
|
1461
|
+
function usageTotals(entry, days) {
|
|
1462
|
+
const map = parseHitDays(entry && entry.hit_days);
|
|
1463
|
+
let total = 0;
|
|
1464
|
+
for (const k of Object.keys(map)) {
|
|
1465
|
+
const age = daysBetween(k.slice(1), today());
|
|
1466
|
+
if (age >= 0 && age <= days) total += map[k];
|
|
1467
|
+
}
|
|
1468
|
+
return total;
|
|
1469
|
+
}
|
|
1333
1470
|
function utilityScore(meta) {
|
|
1334
1471
|
const conf = Math.min(Math.max(parseFloat(meta.confidence || 1.0) || 1.0, 0), 1);
|
|
1335
1472
|
const acc = parseInt(meta.access_count || '0', 10) || 0;
|
|
@@ -2299,6 +2436,8 @@ function readEntry(fp, root) {
|
|
|
2299
2436
|
source: meta.source || '',
|
|
2300
2437
|
weight: (parseFloat(meta.weight) > 0 ? parseFloat(meta.weight) : 1.0),
|
|
2301
2438
|
feedback_net: feedbackNetOf(meta),
|
|
2439
|
+
hit_days: meta.hit_days || '',
|
|
2440
|
+
hit_queries: meta.hit_queries || '',
|
|
2302
2441
|
tokens: buildTokens(subject, statement, tags),
|
|
2303
2442
|
meta: meta,
|
|
2304
2443
|
};
|
|
@@ -2312,23 +2451,72 @@ function rewriteFrontmatter(fp, patch, root, owner) {
|
|
|
2312
2451
|
for (const k of Object.keys(patch)) meta[k] = patch[k];
|
|
2313
2452
|
writeMemoryText(root || memoryRootForFile(fp), fp, frontmatterToText(meta, parsed.body), owner || ownerFromPrivatePath(root || memoryRootForFile(fp), fp));
|
|
2314
2453
|
}
|
|
2315
|
-
function bumpReadMeta(root, relFiles) {
|
|
2454
|
+
function bumpReadMeta(root, relFiles, options) {
|
|
2455
|
+
options = options || {};
|
|
2316
2456
|
const now = today();
|
|
2457
|
+
const settings = usageSettings(options);
|
|
2458
|
+
const selfAgent = (options.selfAgent !== undefined) ? options.selfAgent : resolveIdentity(options).id;
|
|
2459
|
+
const usageByFile = {};
|
|
2317
2460
|
for (const rel of relFiles) {
|
|
2318
2461
|
const fp = path.join(root, rel);
|
|
2319
2462
|
if (!fs.existsSync(fp)) continue;
|
|
2320
2463
|
const owner = ownerFromPrivatePath(root, fp);
|
|
2464
|
+
// v0.18.0:跨 owner 私密条目一律不写(读得到也不代表能改别人的文件)。
|
|
2465
|
+
if (owner && owner !== selfAgent) continue;
|
|
2321
2466
|
let parsed;
|
|
2322
2467
|
try { parsed = parseFrontmatter(readMemoryText(root, fp, owner)); } catch (e) { continue; }
|
|
2323
2468
|
const meta = parsed.meta;
|
|
2324
2469
|
if (meta.immutable === 'true') continue;
|
|
2325
2470
|
const acc = (parseInt(meta.access_count || '0', 10) || 0) + 1;
|
|
2326
|
-
|
|
2471
|
+
const patch = { access_count: acc, last_accessed: now };
|
|
2472
|
+
if (settings.enabled) {
|
|
2473
|
+
const usagePatch = usagePatchForMeta(meta, 'search', options.query || '', settings, now);
|
|
2474
|
+
Object.assign(patch, usagePatch);
|
|
2475
|
+
usageByFile[rel] = usagePatch;
|
|
2476
|
+
}
|
|
2477
|
+
try {
|
|
2478
|
+
rewriteFrontmatter(fp, patch, root, owner);
|
|
2479
|
+
} catch (e) {
|
|
2480
|
+
// 打点写失败不影响读取结果本身;索引也保持原样。
|
|
2481
|
+
delete usageByFile[rel];
|
|
2482
|
+
}
|
|
2327
2483
|
}
|
|
2484
|
+
return { usageByFile: usageByFile };
|
|
2328
2485
|
}
|
|
2329
2486
|
function touchIndex(root, relFiles, options) {
|
|
2487
|
+
options = options || {};
|
|
2330
2488
|
const set = new Set(relFiles);
|
|
2331
2489
|
const now = today();
|
|
2490
|
+
const usageByFile = options.usageByFile || {};
|
|
2491
|
+
const bumpEntry = function (e) {
|
|
2492
|
+
e.access_count = (parseInt(e.access_count, 10) || 0) + 1;
|
|
2493
|
+
e.last_accessed = now;
|
|
2494
|
+
const usage = usageByFile[e.file];
|
|
2495
|
+
if (usage) {
|
|
2496
|
+
if (usage.hit_days !== undefined) e.hit_days = usage.hit_days;
|
|
2497
|
+
if (usage.hit_queries !== undefined) e.hit_queries = usage.hit_queries;
|
|
2498
|
+
}
|
|
2499
|
+
};
|
|
2500
|
+
// v0.18.0:加密库的私密条目命中必须当场同步 owner index(旧版会滞后到 reindex)。
|
|
2501
|
+
if (isEncrypted(root)) {
|
|
2502
|
+
const byOwner = {};
|
|
2503
|
+
for (const rel of set) {
|
|
2504
|
+
if (String(rel).indexOf(PRIVATE_DIR + '/') !== 0) continue;
|
|
2505
|
+
const owner = String(rel).split('/')[1] || '';
|
|
2506
|
+
(byOwner[owner] = byOwner[owner] || []).push(rel);
|
|
2507
|
+
}
|
|
2508
|
+
for (const owner of Object.keys(byOwner)) {
|
|
2509
|
+
const key = getOwnerKeyFor(root, owner);
|
|
2510
|
+
if (!key) continue;
|
|
2511
|
+
let ownerIdx;
|
|
2512
|
+
try { ownerIdx = loadOwnerIndex(root, owner, key); } catch (e) { continue; }
|
|
2513
|
+
let dirty = false;
|
|
2514
|
+
for (const e of ownerIdx) {
|
|
2515
|
+
if (set.has(e.file)) { bumpEntry(e); dirty = true; }
|
|
2516
|
+
}
|
|
2517
|
+
if (dirty) saveOwnerIndex(root, owner, key, ownerIdx);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2332
2520
|
// v0.17.0 B3:带 --year 的检索只碰命中年份的分片(命中条目必然来自这些分片),
|
|
2333
2521
|
// 不传年份时行为与旧版一致(遍历全部 shards)。
|
|
2334
2522
|
const filterYears = (options && Array.isArray(options.years)) ? options.years : [];
|
|
@@ -2344,7 +2532,7 @@ function touchIndex(root, relFiles, options) {
|
|
|
2344
2532
|
if (!sd || !Array.isArray(sd.entries)) continue;
|
|
2345
2533
|
let dirty = false;
|
|
2346
2534
|
for (const e of sd.entries) {
|
|
2347
|
-
if (set.has(e.file)) {
|
|
2535
|
+
if (set.has(e.file)) { bumpEntry(e); dirty = true; }
|
|
2348
2536
|
}
|
|
2349
2537
|
if (dirty) fs.writeFileSync(sp, JSON.stringify(sd, null, 2), 'utf8');
|
|
2350
2538
|
}
|
|
@@ -2353,7 +2541,7 @@ function touchIndex(root, relFiles, options) {
|
|
|
2353
2541
|
const idx = getIndex(root);
|
|
2354
2542
|
let dirty = false;
|
|
2355
2543
|
for (const e of idx) {
|
|
2356
|
-
if (set.has(e.file)) {
|
|
2544
|
+
if (set.has(e.file)) { bumpEntry(e); dirty = true; }
|
|
2357
2545
|
}
|
|
2358
2546
|
if (dirty) saveIndex(root, idx);
|
|
2359
2547
|
}
|
|
@@ -3491,6 +3679,10 @@ const SCALE_THRESHOLD_DEFAULTS = {
|
|
|
3491
3679
|
scale_warn_files_per_dir: 500,
|
|
3492
3680
|
scale_warn_index_bytes: 5 * 1024 * 1024,
|
|
3493
3681
|
scale_warn_cold_start_ms: 2000,
|
|
3682
|
+
scale_info_entries: 30000,
|
|
3683
|
+
scale_info_files_per_dir: 300,
|
|
3684
|
+
scale_info_index_bytes: 3 * 1024 * 1024,
|
|
3685
|
+
scale_info_cold_start_ms: 1200,
|
|
3494
3686
|
};
|
|
3495
3687
|
function scaleThreshold(cfg, key) {
|
|
3496
3688
|
const raw = cfg ? cfg[key] : undefined;
|
|
@@ -3525,6 +3717,10 @@ function measureIndexColdStart(root) {
|
|
|
3525
3717
|
const elapsed = Number(process.hrtime.bigint() - started) / 1e6;
|
|
3526
3718
|
return Math.round(elapsed * 1000) / 1000;
|
|
3527
3719
|
}
|
|
3720
|
+
function scaleMetric(key, value, unit, info, warn) {
|
|
3721
|
+
const level = value > warn ? 'warning' : (value > info ? 'info' : 'ok');
|
|
3722
|
+
return { key: key, value: value, unit: unit, info: info, warn: warn, level: level };
|
|
3723
|
+
}
|
|
3528
3724
|
function scaleReport(root, cfg) {
|
|
3529
3725
|
const thresholds = {};
|
|
3530
3726
|
for (const key of Object.keys(SCALE_THRESHOLD_DEFAULTS)) thresholds[key] = scaleThreshold(cfg, key);
|
|
@@ -3536,26 +3732,37 @@ function scaleReport(root, cfg) {
|
|
|
3536
3732
|
for (let i = 0; i < 3; i++) samples.push(measureIndexColdStart(root));
|
|
3537
3733
|
samples.sort(function (a, b) { return a - b; });
|
|
3538
3734
|
const coldStartMs = samples[Math.floor(samples.length / 2)];
|
|
3735
|
+
const metrics = [
|
|
3736
|
+
scaleMetric('entries', entries, '条', thresholds.scale_info_entries, thresholds.scale_warn_entries),
|
|
3737
|
+
scaleMetric('files_per_dir', filesPerDir, '个文件', thresholds.scale_info_files_per_dir, thresholds.scale_warn_files_per_dir),
|
|
3738
|
+
scaleMetric('index_bytes', indexBytes, '字节', thresholds.scale_info_index_bytes, thresholds.scale_warn_index_bytes),
|
|
3739
|
+
scaleMetric('cold_start_ms', coldStartMs, 'ms', thresholds.scale_info_cold_start_ms, thresholds.scale_warn_cold_start_ms),
|
|
3740
|
+
];
|
|
3539
3741
|
const warnings = [];
|
|
3540
|
-
if (
|
|
3742
|
+
if (metrics[0].level === 'warning') {
|
|
3541
3743
|
warnings.push('规模: 记忆条数 ' + entries + ' 超过阈值 ' + thresholds.scale_warn_entries + '(考虑按年归档旧条目,或分批导出冷数据)');
|
|
3542
3744
|
}
|
|
3543
|
-
if (
|
|
3745
|
+
if (metrics[1].level === 'warning') {
|
|
3544
3746
|
warnings.push('规模: 单个目录文件数 ' + filesPerDir + ' 超过阈值 ' + thresholds.scale_warn_files_per_dir + '(检查是否有目录未按年/月分层)');
|
|
3545
3747
|
}
|
|
3546
|
-
if (
|
|
3748
|
+
if (metrics[2].level === 'warning') {
|
|
3547
3749
|
warnings.push('规模: 索引总体积 ' + indexBytes + ' 字节超过阈值 ' + thresholds.scale_warn_index_bytes + '(可用 reindex 重建,或按年份分片)');
|
|
3548
3750
|
}
|
|
3549
|
-
if (
|
|
3751
|
+
if (metrics[3].level === 'warning') {
|
|
3550
3752
|
warnings.push('规模: 索引冷启动 ' + coldStartMs + 'ms 超过阈值 ' + thresholds.scale_warn_cold_start_ms + 'ms(可用 reindex 重建索引)');
|
|
3551
3753
|
}
|
|
3754
|
+
// v0.18.0 A6:分级只影响 level 与提示文案;超 warn 仍进 warnings(既有行为),info 不阻断。
|
|
3755
|
+
const level = metrics.some(function (m) { return m.level === 'warning'; })
|
|
3756
|
+
? 'warning'
|
|
3757
|
+
: (metrics.some(function (m) { return m.level === 'info'; }) ? 'info' : 'ok');
|
|
3552
3758
|
return {
|
|
3553
3759
|
entries: entries,
|
|
3554
3760
|
files_per_dir: filesPerDir,
|
|
3555
3761
|
index_bytes: indexBytes,
|
|
3556
3762
|
cold_start_ms: coldStartMs,
|
|
3557
3763
|
thresholds: thresholds,
|
|
3558
|
-
|
|
3764
|
+
metrics: metrics,
|
|
3765
|
+
level: level,
|
|
3559
3766
|
warnings: warnings,
|
|
3560
3767
|
};
|
|
3561
3768
|
}
|
|
@@ -3722,7 +3929,13 @@ function doctorCore(opts) {
|
|
|
3722
3929
|
'- 备份目录: ' + (configuredDir || (cfg.backup_setup_choice === 'manual' ? '手动模式' : '(未配置)')),
|
|
3723
3930
|
];
|
|
3724
3931
|
if (checks.scale) {
|
|
3725
|
-
|
|
3932
|
+
const scaleLevelLabel = checks.scale.level === 'warning' ? '告警' : (checks.scale.level === 'info' ? '提示' : '正常');
|
|
3933
|
+
const scaleTop = (checks.scale.metrics || []).filter(function (m) { return m.level !== 'ok'; })
|
|
3934
|
+
.sort(function (a, b) {
|
|
3935
|
+
const rank = { warning: 2, info: 1, ok: 0 };
|
|
3936
|
+
return rank[b.level] - rank[a.level];
|
|
3937
|
+
})[0];
|
|
3938
|
+
lines.push('- 规模: ' + checks.scale.entries + ' 条记忆 / 单目录最大 ' + checks.scale.files_per_dir + ' 个文件 / 索引 ' + checks.scale.index_bytes + ' 字节 / 冷启动 ' + checks.scale.cold_start_ms + 'ms(' + scaleLevelLabel + (scaleTop ? ',最高项 ' + scaleTop.key + ' ' + scaleTop.value + scaleTop.unit : '') + ')');
|
|
3726
3939
|
}
|
|
3727
3940
|
if (checks.layout && (checks.layout.duplicates || checks.layout.conflicts || checks.layout.unverified)) {
|
|
3728
3941
|
lines.push('- 布局: 同序号重复 ' + checks.layout.duplicates + ' 组 / 冲突 ' + checks.layout.conflicts + ' 组 / 待核 ' + checks.layout.unverified + ' 组');
|
|
@@ -4405,11 +4618,19 @@ function recallCore(query, opts) {
|
|
|
4405
4618
|
}
|
|
4406
4619
|
return { error: false, exitCode: 0, text: '无匹配记忆。' };
|
|
4407
4620
|
}
|
|
4408
|
-
const
|
|
4409
|
-
|
|
4621
|
+
const touchAll = shown.filter(function (h) { return !h.entry.immutable; }).map(function (h) { return h.entry.file; });
|
|
4622
|
+
let usageSkipped = 0;
|
|
4623
|
+
if (touchAll.length) {
|
|
4410
4624
|
for (const root of roots) {
|
|
4411
|
-
|
|
4412
|
-
|
|
4625
|
+
const touchRel = touchAll.filter(function (rel) {
|
|
4626
|
+
if (!fs.existsSync(path.join(root, rel))) return false;
|
|
4627
|
+
const owner = ownerFromPrivatePath(root, path.join(root, rel));
|
|
4628
|
+
if (owner && owner !== selfAgent) { usageSkipped++; return false; }
|
|
4629
|
+
return true;
|
|
4630
|
+
});
|
|
4631
|
+
if (!touchRel.length) continue;
|
|
4632
|
+
const bump = bumpReadMeta(root, touchRel, { selfAgent: selfAgent, query: q, noUsage: !!opts.noUsage });
|
|
4633
|
+
touchIndex(root, touchRel, { years: years, usageByFile: bump.usageByFile });
|
|
4413
4634
|
}
|
|
4414
4635
|
}
|
|
4415
4636
|
const lines = ['共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):'];
|
|
@@ -4426,6 +4647,9 @@ function recallCore(query, opts) {
|
|
|
4426
4647
|
if (deniedCount > 0 && explicitCross) {
|
|
4427
4648
|
lines.push('\n[警告] 本次检索共拒绝 ' + deniedCount + ' 条越界访问(其它智能体私密记忆,未授权不展示)。如需读取请加 --unsafe 或 --owner user。');
|
|
4428
4649
|
}
|
|
4650
|
+
if (usageSkipped > 0) {
|
|
4651
|
+
lines.push('\n[提示] 跨 owner 私密条目未记录命中打点(' + usageSkipped + ' 条):只有条目所属 owner 才能更新自己的打点。');
|
|
4652
|
+
}
|
|
4429
4653
|
return {
|
|
4430
4654
|
error: false,
|
|
4431
4655
|
exitCode: 0,
|
|
@@ -4575,28 +4799,200 @@ function forgetCore(fileRef, opts) {
|
|
|
4575
4799
|
}
|
|
4576
4800
|
return { error: false, text: '已移入回收区: ' + trashFile };
|
|
4577
4801
|
}
|
|
4802
|
+
// ---- v0.18.0 A3:容量水位 / 淘汰候选 / 晋升建议(只读报告,不自动动数据)----
|
|
4803
|
+
const CAPACITY_CONFIG_DEFAULTS = {
|
|
4804
|
+
capacity_warn_bytes: 50 * 1024 * 1024,
|
|
4805
|
+
capacity_cooldown_days: 30,
|
|
4806
|
+
capacity_candidate_limit: 20,
|
|
4807
|
+
promotion_min_hits: 3,
|
|
4808
|
+
promotion_min_queries: 3,
|
|
4809
|
+
};
|
|
4810
|
+
function capacityNumber(cfg, key) {
|
|
4811
|
+
const raw = cfg ? cfg[key] : undefined;
|
|
4812
|
+
if (raw === undefined || raw === null || raw === '') return CAPACITY_CONFIG_DEFAULTS[key];
|
|
4813
|
+
const n = parseFloat(raw);
|
|
4814
|
+
return (n >= 0) ? n : CAPACITY_CONFIG_DEFAULTS[key];
|
|
4815
|
+
}
|
|
4816
|
+
function isEvergreenTagged(meta) {
|
|
4817
|
+
// tags 在真实库里可能是 JSON 数组串(["pinned"]),parseTags 会保留引号,这里统一去掉再比较。
|
|
4818
|
+
const tags = parseTags(meta && meta.tags).map(function (t) {
|
|
4819
|
+
return String(t).toLowerCase().replace(/^["']|["']$/g, '');
|
|
4820
|
+
});
|
|
4821
|
+
return tags.indexOf('evergreen') !== -1 || tags.indexOf('pinned') !== -1;
|
|
4822
|
+
}
|
|
4823
|
+
function capacityMemoryBytes(root) {
|
|
4824
|
+
let total = 0;
|
|
4825
|
+
for (const fp of collectEntryFiles(root)) {
|
|
4826
|
+
try { total += fs.statSync(fp).size; } catch (e) { /* 单文件缺失不影响整体水位 */ }
|
|
4827
|
+
}
|
|
4828
|
+
return total;
|
|
4829
|
+
}
|
|
4830
|
+
function capacityCandidateRow(e) {
|
|
4831
|
+
return {
|
|
4832
|
+
file: e.file,
|
|
4833
|
+
type: e.type,
|
|
4834
|
+
subject: e.subject,
|
|
4835
|
+
hits_30d: e.hits_30d,
|
|
4836
|
+
hits_90d: e.hits_90d,
|
|
4837
|
+
last_used: e.last_used,
|
|
4838
|
+
age_days: e.age_days,
|
|
4839
|
+
utility: e.utility,
|
|
4840
|
+
suggested_command: 'yotta-memory explain ' + e.file,
|
|
4841
|
+
};
|
|
4842
|
+
}
|
|
4843
|
+
function capacityCore(opts) {
|
|
4844
|
+
opts = opts || {};
|
|
4845
|
+
const root = path.resolve(opts.root || userRoot());
|
|
4846
|
+
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。', report: null };
|
|
4847
|
+
const cfg = loadConfig();
|
|
4848
|
+
const selfAgent = (opts.selfAgent !== undefined) ? opts.selfAgent : resolveIdentity(opts).id;
|
|
4849
|
+
const cooldownDays = capacityNumber(cfg, 'capacity_cooldown_days');
|
|
4850
|
+
const limit = (opts.limit !== undefined && opts.limit !== null)
|
|
4851
|
+
? (parseInt(opts.limit, 10) || 0)
|
|
4852
|
+
: capacityNumber(cfg, 'capacity_candidate_limit');
|
|
4853
|
+
const minHits = capacityNumber(cfg, 'promotion_min_hits');
|
|
4854
|
+
const minQueries = capacityNumber(cfg, 'promotion_min_queries');
|
|
4855
|
+
const skipped = { cooldown: 0, evergreen: 0, denied: 0, unreadable: 0 };
|
|
4856
|
+
const activityBase = [];
|
|
4857
|
+
const candidatesForCleanup = [];
|
|
4858
|
+
for (const fp of collectEntryFiles(root)) {
|
|
4859
|
+
const r = relOf(root, fp);
|
|
4860
|
+
if (checkOwnerWritable(root, r, selfAgent, opts.unsafe)) { skipped.denied++; continue; }
|
|
4861
|
+
let e;
|
|
4862
|
+
try { e = readEntry(fp, root); } catch (err) { skipped.unreadable++; continue; }
|
|
4863
|
+
e.hits_30d = usageTotals(e, 30);
|
|
4864
|
+
e.hits_90d = usageTotals(e, 90);
|
|
4865
|
+
e.last_used = e.last_accessed || e.created || '';
|
|
4866
|
+
e.age_days = e.created ? daysBetween(e.created, today()) : -1;
|
|
4867
|
+
e.utility = round3(utilityScore(e.meta));
|
|
4868
|
+
activityBase.push(e);
|
|
4869
|
+
if (e.immutable || e.type === 'BOUND' || isEvergreenTagged(e.meta)) { skipped.evergreen++; continue; }
|
|
4870
|
+
if (e.age_days >= 0 && e.age_days < cooldownDays) { skipped.cooldown++; continue; }
|
|
4871
|
+
candidatesForCleanup.push(e);
|
|
4872
|
+
}
|
|
4873
|
+
const byOldest = function (a, b) {
|
|
4874
|
+
const la = a.last_used || '', lb = b.last_used || '';
|
|
4875
|
+
if (la !== lb) return la.localeCompare(lb);
|
|
4876
|
+
return String(a.file).localeCompare(String(b.file));
|
|
4877
|
+
};
|
|
4878
|
+
const lru = candidatesForCleanup.slice().sort(byOldest).slice(0, limit).map(capacityCandidateRow);
|
|
4879
|
+
const lfu = candidatesForCleanup.slice().sort(function (a, b) {
|
|
4880
|
+
if (a.hits_90d !== b.hits_90d) return a.hits_90d - b.hits_90d;
|
|
4881
|
+
return byOldest(a, b);
|
|
4882
|
+
}).slice(0, limit).map(capacityCandidateRow);
|
|
4883
|
+
const promotions = candidatesForCleanup.filter(function (e) {
|
|
4884
|
+
if (e.hits_90d < minHits) return false;
|
|
4885
|
+
if (Object.keys(parseHitQueries(e.hit_queries)).length < minQueries) return false;
|
|
4886
|
+
if (e.weight >= 1.5) return false;
|
|
4887
|
+
if (isConsolidatedSummary(e)) return false;
|
|
4888
|
+
return true;
|
|
4889
|
+
}).map(function (e) {
|
|
4890
|
+
const row = capacityCandidateRow(e);
|
|
4891
|
+
row.distinct_queries = Object.keys(parseHitQueries(e.hit_queries)).length;
|
|
4892
|
+
row.suggested_command = 'yotta-memory feedback ' + e.file + ' --useful';
|
|
4893
|
+
return row;
|
|
4894
|
+
});
|
|
4895
|
+
const scale = scaleReport(root, cfg);
|
|
4896
|
+
const memoryBytes = capacityMemoryBytes(root);
|
|
4897
|
+
const warnBytes = capacityNumber(cfg, 'capacity_warn_bytes');
|
|
4898
|
+
const infoBytes = warnBytes * 0.6;
|
|
4899
|
+
const memoryLevel = memoryBytes > warnBytes ? 'warning' : (memoryBytes > infoBytes ? 'info' : 'ok');
|
|
4900
|
+
const waterLevel = (scale.level === 'warning' || memoryLevel === 'warning')
|
|
4901
|
+
? 'warning'
|
|
4902
|
+
: ((scale.level === 'info' || memoryLevel === 'info') ? 'info' : 'ok');
|
|
4903
|
+
const thresholds = Object.assign({}, scale.thresholds, {
|
|
4904
|
+
capacity_warn_bytes: warnBytes,
|
|
4905
|
+
capacity_cooldown_days: cooldownDays,
|
|
4906
|
+
capacity_candidate_limit: limit,
|
|
4907
|
+
promotion_min_hits: minHits,
|
|
4908
|
+
promotion_min_queries: minQueries,
|
|
4909
|
+
});
|
|
4910
|
+
const water = {
|
|
4911
|
+
entries: scale.entries,
|
|
4912
|
+
files_per_dir: scale.files_per_dir,
|
|
4913
|
+
index_bytes: scale.index_bytes,
|
|
4914
|
+
cold_start_ms: scale.cold_start_ms,
|
|
4915
|
+
memory_bytes: memoryBytes,
|
|
4916
|
+
thresholds: thresholds,
|
|
4917
|
+
metrics: scale.metrics.concat([{
|
|
4918
|
+
key: 'memory_bytes', value: memoryBytes, unit: '字节', info: infoBytes, warn: warnBytes, level: memoryLevel,
|
|
4919
|
+
}]),
|
|
4920
|
+
level: waterLevel,
|
|
4921
|
+
};
|
|
4922
|
+
const activity = {
|
|
4923
|
+
readable: activityBase.length,
|
|
4924
|
+
hits_30d_entries: activityBase.filter(function (e) { return e.hits_30d > 0; }).length,
|
|
4925
|
+
hits_90d_entries: activityBase.filter(function (e) { return e.hits_90d > 0; }).length,
|
|
4926
|
+
never_hit: activityBase.filter(function (e) { return e.hits_90d === 0 && (parseInt(e.access_count, 10) || 0) === 0; }).length,
|
|
4927
|
+
top: activityBase.filter(function (e) { return e.hits_90d > 0; })
|
|
4928
|
+
.sort(function (a, b) {
|
|
4929
|
+
if (b.hits_90d !== a.hits_90d) return b.hits_90d - a.hits_90d;
|
|
4930
|
+
return byOldest(a, b);
|
|
4931
|
+
})
|
|
4932
|
+
.slice(0, Math.min(limit, 10))
|
|
4933
|
+
.map(capacityCandidateRow),
|
|
4934
|
+
};
|
|
4935
|
+
const lines = ['## yotta-memory maintain --capacity(容量水位)'];
|
|
4936
|
+
const waterLabel = waterLevel === 'warning' ? '告警' : (waterLevel === 'info' ? '提示' : '正常');
|
|
4937
|
+
lines.push('- 水位: ' + waterLabel
|
|
4938
|
+
+ '(条目 ' + scale.entries + ' / 记忆文件 ' + memoryBytes + ' 字节(阈值 ' + warnBytes + ')'
|
|
4939
|
+
+ ' / 索引 ' + scale.index_bytes + ' 字节 / 单目录最大 ' + scale.files_per_dir + ' 个文件 / 冷启动 ' + scale.cold_start_ms + 'ms)');
|
|
4940
|
+
lines.push('- 活跃度: 可读 ' + activity.readable + ' 条;30 天命中 ' + activity.hits_30d_entries
|
|
4941
|
+
+ ' 条 / 90 天命中 ' + activity.hits_90d_entries + ' 条 / 无命中记录 ' + activity.never_hit + ' 条');
|
|
4942
|
+
lines.push('');
|
|
4943
|
+
lines.push('### LRU 淘汰候选(最久未用,' + lru.length + ' 条)');
|
|
4944
|
+
if (!lru.length) lines.push('(无)');
|
|
4945
|
+
for (const row of lru) {
|
|
4946
|
+
lines.push('- [' + row.type + '] ' + row.file + '(90 天命中 ' + row.hits_90d + ',' + row.age_days + ' 天龄,utility ' + row.utility + ')— ' + row.subject);
|
|
4947
|
+
}
|
|
4948
|
+
lines.push('');
|
|
4949
|
+
lines.push('### LFU 淘汰候选(命中最少,' + lfu.length + ' 条)');
|
|
4950
|
+
if (!lfu.length) lines.push('(无)');
|
|
4951
|
+
for (const row of lfu) {
|
|
4952
|
+
lines.push('- [' + row.type + '] ' + row.file + '(90 天命中 ' + row.hits_90d + ',最久未用 ' + row.last_used + ')— ' + row.subject);
|
|
4953
|
+
}
|
|
4954
|
+
lines.push('');
|
|
4955
|
+
lines.push('### 晋升建议(只出建议,不自动改权重,' + promotions.length + ' 条)');
|
|
4956
|
+
if (!promotions.length) lines.push('(无:需要 90 天命中 ≥ ' + minHits + ' 且不同查询 ≥ ' + minQueries + ' 且 weight < 1.5)');
|
|
4957
|
+
for (const row of promotions) {
|
|
4958
|
+
lines.push('- ' + row.file + '(90 天命中 ' + row.hits_90d + ',不同查询 ' + row.distinct_queries + ')→ ' + row.suggested_command);
|
|
4959
|
+
}
|
|
4960
|
+
lines.push('');
|
|
4961
|
+
lines.push('### 跳过(不进候选)');
|
|
4962
|
+
lines.push('- 冷却期(<' + cooldownDays + ' 天): ' + skipped.cooldown + ' 条 / 常青(immutable、BOUND、evergreen、pinned): '
|
|
4963
|
+
+ skipped.evergreen + ' 条 / 其它 owner: ' + skipped.denied + ' 条 / 无法读取: ' + skipped.unreadable + ' 条');
|
|
4964
|
+
lines.push('- 本命令只读:不改记忆、不改权重;归档请显式执行 yotta-memory archive。');
|
|
4965
|
+
const report = {
|
|
4966
|
+
schemaVersion: 1,
|
|
4967
|
+
root: root,
|
|
4968
|
+
thresholds: thresholds,
|
|
4969
|
+
water: water,
|
|
4970
|
+
activity: activity,
|
|
4971
|
+
candidates: { lru: lru, lfu: lfu },
|
|
4972
|
+
promotions: promotions,
|
|
4973
|
+
skipped: skipped,
|
|
4974
|
+
};
|
|
4975
|
+
return { error: false, text: lines.join('\n'), report: report };
|
|
4976
|
+
}
|
|
4578
4977
|
function archiveCore(opts) {
|
|
4579
4978
|
opts = opts || {};
|
|
4580
4979
|
const cfg = loadConfig();
|
|
4581
4980
|
const days = opts.days || parseInt(cfg.maintain_archived_age || '180', 10) || 180;
|
|
4582
4981
|
const threshold = (opts.threshold !== undefined && opts.threshold !== null) ? opts.threshold : (parseFloat(cfg.maintain_archived_utility || 0.35));
|
|
4982
|
+
const cooldownDays = capacityNumber(cfg, 'capacity_cooldown_days');
|
|
4583
4983
|
const root = userRoot();
|
|
4584
4984
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
action: 'archive',
|
|
4588
|
-
snapshotDir: opts.snapshotDir,
|
|
4589
|
-
backupDir: opts.backupDir,
|
|
4590
|
-
allowSameVolumeForTest: opts.allowSameVolumeForTest,
|
|
4591
|
-
sameVolumeFn: opts.sameVolumeFn,
|
|
4592
|
-
now: opts.now,
|
|
4593
|
-
});
|
|
4594
|
-
if (guard.error) return { error: true, text: guard.text };
|
|
4985
|
+
const dryRun = !!opts.dryRun;
|
|
4986
|
+
const force = !!opts.force;
|
|
4595
4987
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
4596
|
-
let moved = 0;
|
|
4597
|
-
let deniedCrossOwner = 0;
|
|
4598
|
-
const movedFiles = [];
|
|
4599
4988
|
const selfAgent = resolveIdentity(opts).id;
|
|
4989
|
+
// v0.18.0 修正:先只读扫描候选与跳过统计,再决定要不要过破坏性闸门 ——
|
|
4990
|
+
// 预演(--dry-run)与「没有候选」两种情况下都不写盘。
|
|
4991
|
+
// 旧行为是进门就建整库快照 + 写审计,且 --dry-run 标志被静默吞掉(照常归档)。
|
|
4992
|
+
const candidates = [];
|
|
4993
|
+
let deniedCrossOwner = 0;
|
|
4994
|
+
let skippedCooldown = 0;
|
|
4995
|
+
let skippedEvergreen = 0;
|
|
4600
4996
|
for (const fp of collectEntryFiles(root)) {
|
|
4601
4997
|
const owner = ownerFromPrivatePath(root, fp);
|
|
4602
4998
|
// v0.17.2:只归档自己有权写的条目 —— 旧行为遍历全部 owner,
|
|
@@ -4613,23 +5009,86 @@ function archiveCore(opts) {
|
|
|
4613
5009
|
if (isNaN(createdTs)) continue;
|
|
4614
5010
|
const t = (meta.type || 'FACT').toUpperCase();
|
|
4615
5011
|
if (t === 'BOUND') continue; // v0.10.0 BOUND 豁免归档(边界常驻)
|
|
5012
|
+
if (!force && isEvergreenTagged(meta)) { skippedEvergreen++; continue; }
|
|
5013
|
+
const ageDays = meta.created ? daysBetween(meta.created, today()) : -1;
|
|
5014
|
+
if (!force && ageDays >= 0 && ageDays < cooldownDays) { skippedCooldown++; continue; }
|
|
4616
5015
|
// v0.8.0 统一效用分(盖棺分)替代 vitality
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
5016
|
+
const score = utilityScore(meta);
|
|
5017
|
+
if (score < threshold && createdTs < cutoff) {
|
|
5018
|
+
candidates.push({
|
|
5019
|
+
fp: fp,
|
|
5020
|
+
owner: owner,
|
|
5021
|
+
entry: {
|
|
5022
|
+
file: relOf(root, fp),
|
|
5023
|
+
type: t,
|
|
5024
|
+
subject: String(meta.subject || ''),
|
|
5025
|
+
age_days: ageDays,
|
|
5026
|
+
utility: round3(score),
|
|
5027
|
+
},
|
|
5028
|
+
});
|
|
4625
5029
|
}
|
|
4626
5030
|
}
|
|
4627
|
-
|
|
4628
|
-
let text = '已归档 ' + moved + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) + '\n事务快照: ' + guard.snapshot.id;
|
|
5031
|
+
const skippedLines = [];
|
|
4629
5032
|
if (deniedCrossOwner) {
|
|
4630
|
-
|
|
5033
|
+
skippedLines.push('跨 owner 私密条目已跳过(' + deniedCrossOwner + ' 条):请用 --agent <id> 声明自己的身份;只有用户显式授权(--unsafe)才能越界维护。');
|
|
4631
5034
|
}
|
|
4632
|
-
|
|
5035
|
+
if (skippedCooldown || skippedEvergreen) {
|
|
5036
|
+
skippedLines.push('因冷却期 / 常青豁免跳过 ' + (skippedCooldown + skippedEvergreen) + ' 条(冷却期 ' + skippedCooldown + ' / 常青 ' + skippedEvergreen + ');确认要一并归档时加 --force(immutable 与 BOUND 仍豁免)。');
|
|
5037
|
+
}
|
|
5038
|
+
const report = {
|
|
5039
|
+
schemaVersion: 1,
|
|
5040
|
+
root: root,
|
|
5041
|
+
mode: dryRun ? 'dry-run' : 'apply',
|
|
5042
|
+
days: days,
|
|
5043
|
+
threshold: threshold,
|
|
5044
|
+
cooldown_days: cooldownDays,
|
|
5045
|
+
force: force,
|
|
5046
|
+
candidates: candidates.map(function (c) { return c.entry; }),
|
|
5047
|
+
archived: [],
|
|
5048
|
+
skipped: { cooldown: skippedCooldown, evergreen: skippedEvergreen, cross_owner: deniedCrossOwner },
|
|
5049
|
+
};
|
|
5050
|
+
if (dryRun) {
|
|
5051
|
+
// 预演只读:不动文件、不建事务快照、不写审计、不改索引。
|
|
5052
|
+
const lines = ['预览(未改动): 将归档 ' + candidates.length + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR)];
|
|
5053
|
+
lines.push('- 判定: 效用 < ' + threshold + ' 且未使用 > ' + days + ' 天' + (force
|
|
5054
|
+
? '(--force:冷却期与常青标签不再豁免)'
|
|
5055
|
+
: '(冷却期 ' + cooldownDays + ' 天内与 immutable / BOUND / evergreen / pinned 豁免)'));
|
|
5056
|
+
for (const c of candidates) {
|
|
5057
|
+
lines.push('- [' + c.entry.type + '] ' + c.entry.file + '(' + c.entry.age_days + ' 天龄,utility ' + c.entry.utility + ')— ' + c.entry.subject);
|
|
5058
|
+
}
|
|
5059
|
+
if (!candidates.length) lines.push('(无符合条件的记忆)');
|
|
5060
|
+
lines.push('- 预演只读:未创建事务快照、未写审计、未移动文件;去掉 --dry-run 才执行(执行前先过破坏性写入闸门:doctor + 独立备份目录 + 异卷)。');
|
|
5061
|
+
lines.push.apply(lines, skippedLines);
|
|
5062
|
+
return { error: false, dryRun: true, text: lines.join('\n'), report: report };
|
|
5063
|
+
}
|
|
5064
|
+
if (!candidates.length) {
|
|
5065
|
+
// 无候选 = 无写入:不创建整库事务快照,也不写审计。
|
|
5066
|
+
const lines = ['已归档 0 条旧记忆到 ' + path.join(root, ARCHIVE_DIR), '没有符合条件的记忆:未创建事务快照(无写入)。'];
|
|
5067
|
+
lines.push.apply(lines, skippedLines);
|
|
5068
|
+
return { error: false, text: lines.join('\n'), report: report };
|
|
5069
|
+
}
|
|
5070
|
+
const guard = destructiveGuardCore({
|
|
5071
|
+
root: root,
|
|
5072
|
+
action: 'archive',
|
|
5073
|
+
snapshotDir: opts.snapshotDir,
|
|
5074
|
+
backupDir: opts.backupDir,
|
|
5075
|
+
allowSameVolumeForTest: opts.allowSameVolumeForTest,
|
|
5076
|
+
sameVolumeFn: opts.sameVolumeFn,
|
|
5077
|
+
now: opts.now,
|
|
5078
|
+
});
|
|
5079
|
+
if (guard.error) return { error: true, text: guard.text };
|
|
5080
|
+
const movedFiles = [];
|
|
5081
|
+
for (const c of candidates) {
|
|
5082
|
+
const dest = path.join(root, archiveRelFor(root, c.entry.file, c.entry.type, c.owner));
|
|
5083
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
5084
|
+
fs.renameSync(c.fp, dest);
|
|
5085
|
+
movedFiles.push(c.entry.file);
|
|
5086
|
+
}
|
|
5087
|
+
if (movedFiles.length) removeIndexEntries(root, movedFiles);
|
|
5088
|
+
report.archived = movedFiles;
|
|
5089
|
+
let text = '已归档 ' + movedFiles.length + ' 条旧记忆到 ' + path.join(root, ARCHIVE_DIR) + '\n事务快照: ' + guard.snapshot.id;
|
|
5090
|
+
if (skippedLines.length) text += '\n' + skippedLines.join('\n');
|
|
5091
|
+
return { error: false, text: text, report: report };
|
|
4633
5092
|
}
|
|
4634
5093
|
|
|
4635
5094
|
// ---- v0.8.0 自我学习/自我进化/自我提升:feedback / explain / maintain / distill ----
|
|
@@ -4691,6 +5150,10 @@ function feedbackCore(ref, opts) {
|
|
|
4691
5150
|
const before = { weight: round3(weight), confidence: round3(confidence), feedback_net: fb };
|
|
4692
5151
|
const after = { weight: round3(nw), confidence: round3(nc), feedback_net: nfb };
|
|
4693
5152
|
const patch = { weight: after.weight, confidence: after.confidence, feedback_net: after.feedback_net, updated: today() };
|
|
5153
|
+
if (useful) {
|
|
5154
|
+
const usageCfg = usageSettings(opts);
|
|
5155
|
+
if (usageCfg.enabled) Object.assign(patch, usagePatchForMeta(meta, 'use', '', usageCfg, today()));
|
|
5156
|
+
}
|
|
4694
5157
|
rewriteFrontmatter(target.fp, patch, target.root, owner);
|
|
4695
5158
|
upsertIndexEntry(target.root, readEntry(target.fp, target.root));
|
|
4696
5159
|
appendAudit(target.root, 'feedback', { ts: new Date().toISOString(), file: target.rel, action: useful ? 'useful' : 'useless', before: before, after: after, reason: opts.reason || '' });
|
|
@@ -4734,6 +5197,7 @@ function explainCore(ref, opts) {
|
|
|
4734
5197
|
// v0.17.2:跨 owner 私密条目 fail-closed —— 旧行为只是加一句「仅元数据」,
|
|
4735
5198
|
// 正文(subject / statement)照样输出,等于把别人的私密记忆读给调用方。
|
|
4736
5199
|
if (deny) return { error: true, text: deny };
|
|
5200
|
+
applyUsageHits(target.root, [target.rel], 'get', '', { selfAgent: selfAgent, noUsage: !!opts.noUsage });
|
|
4737
5201
|
const ub = utilityBreakdown(entry);
|
|
4738
5202
|
const ageDays = entry.created ? daysBetween(entry.created, today()) : 0;
|
|
4739
5203
|
const cfg = loadConfig();
|
|
@@ -4772,12 +5236,100 @@ function subjectsSimilar(a, b) {
|
|
|
4772
5236
|
return editDistance(sa, sb) <= 3;
|
|
4773
5237
|
}
|
|
4774
5238
|
// 记忆自组织:规则层自动归档/遗忘/去重(默认 dry-run,--apply 才执行,--purge 才真删)
|
|
5239
|
+
// ---- v0.18.1 A10:重复踩坑 → 建议升级为规则(只读报告)----
|
|
5240
|
+
function normalizeRuleSubject(s) {
|
|
5241
|
+
return String(s || '').toLowerCase().replace(/[^\u4e00-\u9fa5a-z]+/g, '');
|
|
5242
|
+
}
|
|
5243
|
+
function ruleGroupKeyFor(entry) {
|
|
5244
|
+
for (const t of (entry.tags || [])) {
|
|
5245
|
+
const m = /^pattern-key:(.+)$/i.exec(String(t));
|
|
5246
|
+
if (m) return { key: 'pattern-key:' + m[1].trim(), kind: 'pattern-key' };
|
|
5247
|
+
}
|
|
5248
|
+
const sm = /^yotta-learn:\s*(.+)$/i.exec(String(entry.subject || ''));
|
|
5249
|
+
const cm = /^\[([a-z_]+)\]/i.exec(String(entry.statement || '').trim());
|
|
5250
|
+
if (sm && cm) return { key: cm[1].toLowerCase() + '@' + sm[1].trim(), kind: 'yotta-learn' };
|
|
5251
|
+
const domains = (entry.tags || []).filter(function (t) {
|
|
5252
|
+
return !distillHasWord(t, DISTILL_LESSON_WORDS) && !/^pattern-key:/i.test(String(t)) && String(t) !== '规则' && String(t) !== '边界';
|
|
5253
|
+
}).map(function (t) { return String(t).toLowerCase(); }).sort();
|
|
5254
|
+
if (domains.length) return { key: domains.join('+'), kind: 'tags' };
|
|
5255
|
+
return { key: normalizeRuleSubject(entry.subject), kind: 'subject' };
|
|
5256
|
+
}
|
|
5257
|
+
function isRuleLessonCandidate(entry) {
|
|
5258
|
+
const hay = (entry.tags || []).join(',') + ' ' + String(entry.subject || '');
|
|
5259
|
+
if (distillHasWord(hay, DISTILL_LESSON_WORDS)) return true;
|
|
5260
|
+
const head = String(entry.statement || '').trim().slice(0, 40);
|
|
5261
|
+
return /^\[([a-z_]+)\]/i.test(head) && distillHasWord(head, DISTILL_LESSON_WORDS);
|
|
5262
|
+
}
|
|
5263
|
+
function ruleMiningCore(root, opts) {
|
|
5264
|
+
opts = opts || {};
|
|
5265
|
+
const cfg = loadConfig();
|
|
5266
|
+
const rawMin = (opts.ruleMinHits !== undefined && opts.ruleMinHits !== null) ? opts.ruleMinHits : cfg.maintain_rule_min_hits;
|
|
5267
|
+
const minHits = Math.max(2, parseInt(rawMin || '3', 10) || 3);
|
|
5268
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
5269
|
+
const buckets = {};
|
|
5270
|
+
let scanned = 0, candidates = 0, denied = 0;
|
|
5271
|
+
for (const fp of collectEntryFiles(root)) {
|
|
5272
|
+
const rel = relOf(root, fp);
|
|
5273
|
+
if (checkOwnerWritable(root, rel, selfAgent, opts.unsafe)) { denied += 1; continue; }
|
|
5274
|
+
let e;
|
|
5275
|
+
try { e = readEntry(fp, root); } catch (err) { continue; }
|
|
5276
|
+
scanned += 1;
|
|
5277
|
+
if (['COMMIT', 'FACT', 'PREF'].indexOf(e.type) === -1) continue;
|
|
5278
|
+
if (!isRuleLessonCandidate(e)) continue;
|
|
5279
|
+
const gk = ruleGroupKeyFor(e);
|
|
5280
|
+
if (!gk.key) continue;
|
|
5281
|
+
candidates += 1;
|
|
5282
|
+
(buckets[gk.key] = buckets[gk.key] || []).push({ e: e, kind: gk.kind, file: rel });
|
|
5283
|
+
}
|
|
5284
|
+
const groups = Object.keys(buckets).map(function (key) {
|
|
5285
|
+
const items = buckets[key].slice().sort(function (a, b) {
|
|
5286
|
+
return String(b.e.updated || b.e.created || '').localeCompare(String(a.e.updated || a.e.created || ''));
|
|
5287
|
+
});
|
|
5288
|
+
const best = items.slice().sort(function (a, b) { return (b.e.confidence || 0) - (a.e.confidence || 0); })[0].e;
|
|
5289
|
+
const dates = items.map(function (x) { return x.e.created || x.e.updated || ''; }).filter(Boolean).sort();
|
|
5290
|
+
return {
|
|
5291
|
+
key: key,
|
|
5292
|
+
kind: items[0].kind,
|
|
5293
|
+
count: items.length,
|
|
5294
|
+
files: items.map(function (x) { return x.file; }),
|
|
5295
|
+
span: dates.length ? dates[0] + ' ~ ' + dates[dates.length - 1] : '',
|
|
5296
|
+
best: best,
|
|
5297
|
+
};
|
|
5298
|
+
}).filter(function (g) { return g.count >= minHits; }).sort(function (a, b) {
|
|
5299
|
+
return b.count - a.count || a.key.localeCompare(b.key);
|
|
5300
|
+
});
|
|
5301
|
+
return { error: false, groups: groups, scanned: scanned, candidates: candidates, denied: denied, minHits: minHits };
|
|
5302
|
+
}
|
|
5303
|
+
function ruleMiningLines(result) {
|
|
5304
|
+
const lines = ['### 建议升级为规则(只读,不自动写入)'];
|
|
5305
|
+
if (!result.groups.length) {
|
|
5306
|
+
lines.push('- (无重复踩坑组达到阈值:同组 ≥ ' + result.minHits + ' 条;可用 config set maintain_rule_min_hits N 调整)');
|
|
5307
|
+
return lines;
|
|
5308
|
+
}
|
|
5309
|
+
for (const g of result.groups) {
|
|
5310
|
+
lines.push('- 组键: ' + g.key + '(' + g.count + ' 条' + (g.span ? ',' + g.span : '') + ')');
|
|
5311
|
+
lines.push(' 代表: [' + g.best.type + '] ' + (g.best.subject || '') + ' — ' + String(g.best.statement || '').slice(0, 60));
|
|
5312
|
+
lines.push(' 建议: 人工确认后执行 yotta-memory remember BOUND "规则:' + g.key + '" "<陈述>"');
|
|
5313
|
+
}
|
|
5314
|
+
lines.push('- 说明: 元忆只给晋升信号,不自动写 BOUND;与元习 .learnings/ 的分工见 SKILL「记忆守则」。');
|
|
5315
|
+
return lines;
|
|
5316
|
+
}
|
|
5317
|
+
|
|
4775
5318
|
function maintainCore(opts) {
|
|
4776
5319
|
opts = opts || {};
|
|
4777
5320
|
const apply = !!opts.apply;
|
|
4778
5321
|
const purge = !!opts.purge;
|
|
4779
5322
|
const root = userRoot();
|
|
4780
5323
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
5324
|
+
if (opts.rules && (apply || purge || opts.dedup)) {
|
|
5325
|
+
return { error: true, exitCode: 2, text: '拒绝: --rules 是只读筛选,不能与 --apply / --purge / --dedup 同时使用。' };
|
|
5326
|
+
}
|
|
5327
|
+
if (opts.rules) {
|
|
5328
|
+
const lines = ['## yotta-memory maintain --rules(重复踩坑 → 规则晋升建议)', ''];
|
|
5329
|
+
const result = ruleMiningCore(root, opts);
|
|
5330
|
+
lines.push.apply(lines, ruleMiningLines(result));
|
|
5331
|
+
return { error: false, text: lines.join('\n'), report: result };
|
|
5332
|
+
}
|
|
4781
5333
|
let guard = null;
|
|
4782
5334
|
if (apply) {
|
|
4783
5335
|
guard = destructiveGuardCore({
|
|
@@ -4886,6 +5438,13 @@ function maintainCore(opts) {
|
|
|
4886
5438
|
lines.push('');
|
|
4887
5439
|
lines.push('- 跨 owner 私密条目已跳过(' + deniedCrossOwner + ' 条):请用 --agent <id> 声明自己的身份;只有用户显式授权(--unsafe)才能越界维护。');
|
|
4888
5440
|
}
|
|
5441
|
+
if (!opts.dedup) {
|
|
5442
|
+
const ruleResult = ruleMiningCore(root, opts);
|
|
5443
|
+
if (ruleResult.groups.length) {
|
|
5444
|
+
lines.push('');
|
|
5445
|
+
lines.push.apply(lines, ruleMiningLines(ruleResult));
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
4889
5448
|
// 去重(v0.10.0:置信度分档 + --apply 自动合并;--dedup 与归档/遗忘互斥)
|
|
4890
5449
|
if (opts.dedup) appendDedupBlock(lines, root, opts);
|
|
4891
5450
|
return { error: false, text: lines.join('\n') };
|
|
@@ -4932,7 +5491,263 @@ function mergeCore(refA, refB, opts) {
|
|
|
4932
5491
|
appendAudit(root, 'audit', { ts: new Date().toISOString(), file: drop.file, action: 'merge', reason: '并入 ' + keep.file, utility: round3(utilityScore(keep)) });
|
|
4933
5492
|
return { error: false, text: '已合并: ' + drop.file + ' -> ' + keep.file + '\n 保留: [' + keep.type + '] ' + keep.subject + ': ' + keep.statement + '\n 归档低 confidence: ' + drop.file + '\n 事务快照: ' + guard.snapshot.id };
|
|
4934
5493
|
}
|
|
4935
|
-
//
|
|
5494
|
+
// ---- v0.18.1 A8:蒸馏分类型提取 / 溯源锚点 / 质量指标(确定性,零依赖)----
|
|
5495
|
+
const DISTILL_CLASS_NAMES = ['教训', '待办', '成长', '事件', '规则边界', '其他'];
|
|
5496
|
+
const DISTILL_LESSON_WORDS = ['教训', '踩坑', '失误', '错误', '故障', '事故', '纠正', 'learning', 'error', 'correction', 'incident'];
|
|
5497
|
+
const DISTILL_TASK_WORDS = ['待办', '任务', 'action', 'next', '跟进'];
|
|
5498
|
+
const DISTILL_TASK_TAG_RE = /^t(o)?do$/i;
|
|
5499
|
+
const DISTILL_GROWTH_WORDS = ['成长', '改进', '提升', '洞见', '最佳实践', '复盘', 'insight', 'growth', 'improvement', 'best-practice'];
|
|
5500
|
+
const DISTILL_EVENT_WORDS = ['事件', '里程碑', '发布', '上线', '会议', '活动', 'release', 'milestone'];
|
|
5501
|
+
const DISTILL_FIELD_LIMIT = 500;
|
|
5502
|
+
function distillHasWord(haystack, words) {
|
|
5503
|
+
const s = String(haystack || '').toLowerCase();
|
|
5504
|
+
for (const w of words) if (s.indexOf(String(w).toLowerCase()) !== -1) return true;
|
|
5505
|
+
return false;
|
|
5506
|
+
}
|
|
5507
|
+
function distillClassify(entry, meta) {
|
|
5508
|
+
if (!meta || !meta.type) return '其他';
|
|
5509
|
+
if (entry.type === 'BOUND') return '规则边界';
|
|
5510
|
+
const hay = (entry.tags || []).join(',') + ' ' + String(entry.subject || '');
|
|
5511
|
+
if (distillHasWord(hay, DISTILL_LESSON_WORDS)) return '教训';
|
|
5512
|
+
const hasTaskTag = (entry.tags || []).some(function (t) { return DISTILL_TASK_TAG_RE.test(String(t).trim()); });
|
|
5513
|
+
if (distillHasWord(hay, DISTILL_TASK_WORDS) || hasTaskTag) return '待办';
|
|
5514
|
+
if (distillHasWord(hay, DISTILL_GROWTH_WORDS)) return '成长';
|
|
5515
|
+
if (distillHasWord(hay, DISTILL_EVENT_WORDS)) return '事件';
|
|
5516
|
+
if (entry.type === 'COMMIT') return '待办';
|
|
5517
|
+
if (entry.type === 'PREF') return '成长';
|
|
5518
|
+
if (entry.type === 'FACT') return '事件';
|
|
5519
|
+
return '其他';
|
|
5520
|
+
}
|
|
5521
|
+
function distillSentences(body) {
|
|
5522
|
+
return String(body || '').replace(/([。!?])/g, '$1\n').split('\n').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
5523
|
+
}
|
|
5524
|
+
function distillLineOf(lines, needle) {
|
|
5525
|
+
const n = String(needle || '');
|
|
5526
|
+
if (!n) return 0;
|
|
5527
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].indexOf(n) !== -1) return i + 1;
|
|
5528
|
+
return 0;
|
|
5529
|
+
}
|
|
5530
|
+
function distillClamp(value, limit) {
|
|
5531
|
+
const s = String(value === undefined || value === null ? '' : value);
|
|
5532
|
+
const max = limit || DISTILL_FIELD_LIMIT;
|
|
5533
|
+
if (s.length <= max) return { value: s, truncated: false };
|
|
5534
|
+
return { value: s.slice(0, max) + '…(截断 ' + (s.length - max) + ' 字符,全文见溯源)', truncated: true };
|
|
5535
|
+
}
|
|
5536
|
+
function distillDateNear(sentences, body) {
|
|
5537
|
+
const dateRe = /\d{4}-\d{2}-\d{2}/;
|
|
5538
|
+
for (const s of sentences) {
|
|
5539
|
+
if (dateRe.test(s) && /(截止|前完成|之前|deadline|due)/i.test(s)) return s.match(dateRe)[0];
|
|
5540
|
+
}
|
|
5541
|
+
const all = String(body || '');
|
|
5542
|
+
if (/(截止|前完成|deadline|due)/i.test(all)) {
|
|
5543
|
+
const m = all.match(dateRe);
|
|
5544
|
+
if (m) return m[0];
|
|
5545
|
+
}
|
|
5546
|
+
return '';
|
|
5547
|
+
}
|
|
5548
|
+
function distillExtract(entry, cls, body, lines) {
|
|
5549
|
+
const sents = distillSentences(body);
|
|
5550
|
+
const statement = String(entry.statement || '');
|
|
5551
|
+
const subject = String(entry.subject || '');
|
|
5552
|
+
const tags = (entry.tags || []).join(', ');
|
|
5553
|
+
const fields = {};
|
|
5554
|
+
const used = [];
|
|
5555
|
+
const take = function (name, value) {
|
|
5556
|
+
const ok = value !== undefined && value !== null && String(value) !== '';
|
|
5557
|
+
fields[name] = ok ? String(value) : '未记录';
|
|
5558
|
+
if (ok) used.push(String(value));
|
|
5559
|
+
};
|
|
5560
|
+
let fieldNames = [];
|
|
5561
|
+
if (cls === '教训') {
|
|
5562
|
+
fieldNames = ['教训', '触发条件', '规避方法'];
|
|
5563
|
+
const avoid = sents.filter(function (s) { return /(避免|应该|下次|教训)/.test(s); })[0];
|
|
5564
|
+
take('教训', statement);
|
|
5565
|
+
take('触发条件', tags || subject);
|
|
5566
|
+
take('规避方法', avoid);
|
|
5567
|
+
if (avoid) used.push(avoid);
|
|
5568
|
+
} else if (cls === '待办') {
|
|
5569
|
+
fieldNames = ['任务', '优先级', '截止'];
|
|
5570
|
+
let priority = '';
|
|
5571
|
+
for (const t of (entry.tags || [])) if (/^(high|medium|low|高|中|低)$/i.test(String(t))) { priority = t; break; }
|
|
5572
|
+
const due = distillDateNear(sents.concat([statement]), body + ' ' + statement);
|
|
5573
|
+
take('任务', statement);
|
|
5574
|
+
take('优先级', priority);
|
|
5575
|
+
take('截止', due);
|
|
5576
|
+
if (due) used.push(due);
|
|
5577
|
+
} else if (cls === '成长') {
|
|
5578
|
+
fieldNames = ['改进点', '前后对比', '效果'];
|
|
5579
|
+
const before = sents.filter(function (s) { return /(之前|后来|从.+到)/.test(s); })[0];
|
|
5580
|
+
const effect = sents.filter(function (s) { return /(效果|提升|%|更好)/.test(s); })[0];
|
|
5581
|
+
take('改进点', statement);
|
|
5582
|
+
take('前后对比', before);
|
|
5583
|
+
take('效果', effect);
|
|
5584
|
+
if (before) used.push(before);
|
|
5585
|
+
if (effect) used.push(effect);
|
|
5586
|
+
} else if (cls === '事件') {
|
|
5587
|
+
fieldNames = ['时间', '内容', '结果'];
|
|
5588
|
+
const last = sents.length ? sents[sents.length - 1] : '';
|
|
5589
|
+
take('时间', entry.created || entry.updated);
|
|
5590
|
+
take('内容', subject + (statement ? ': ' + statement : ''));
|
|
5591
|
+
take('结果', last);
|
|
5592
|
+
if (last) used.push(last);
|
|
5593
|
+
} else if (cls === '规则边界') {
|
|
5594
|
+
fieldNames = ['规则', '适用范围', '例外'];
|
|
5595
|
+
const ex = sents.filter(function (s) { return /(除非|例外)/.test(s); })[0];
|
|
5596
|
+
take('规则', statement);
|
|
5597
|
+
take('适用范围', tags || subject);
|
|
5598
|
+
take('例外', ex);
|
|
5599
|
+
if (ex) used.push(ex);
|
|
5600
|
+
} else {
|
|
5601
|
+
fieldNames = ['内容'];
|
|
5602
|
+
take('内容', statement || (sents[0] || String(body || '').trim().slice(0, 200)));
|
|
5603
|
+
}
|
|
5604
|
+
const lineNumbers = used.map(function (v) { return distillLineOf(lines, v); }).filter(Boolean);
|
|
5605
|
+
if (!lineNumbers.length) {
|
|
5606
|
+
const fallback = distillLineOf(lines, statement) || distillLineOf(lines, subject) || 1;
|
|
5607
|
+
lineNumbers.push(fallback);
|
|
5608
|
+
}
|
|
5609
|
+
let truncated = false;
|
|
5610
|
+
for (const name of fieldNames) {
|
|
5611
|
+
if (fields[name] === '未记录') continue;
|
|
5612
|
+
const clamped = distillClamp(fields[name]);
|
|
5613
|
+
if (clamped.truncated) { fields[name] = clamped.value; truncated = true; }
|
|
5614
|
+
}
|
|
5615
|
+
return {
|
|
5616
|
+
fields: fields,
|
|
5617
|
+
fieldNames: fieldNames,
|
|
5618
|
+
anchor: { start: Math.min.apply(null, lineNumbers), end: Math.max.apply(null, lineNumbers) },
|
|
5619
|
+
truncated: truncated,
|
|
5620
|
+
};
|
|
5621
|
+
}
|
|
5622
|
+
function distillElementStats(entry, cls, body) {
|
|
5623
|
+
const sents = distillSentences(body);
|
|
5624
|
+
const tags = (entry.tags || []).join(', ');
|
|
5625
|
+
const hasPriority = (entry.tags || []).some(function (t) { return /^(high|medium|low|高|中|低)$/i.test(String(t)); });
|
|
5626
|
+
let specs = [];
|
|
5627
|
+
if (cls === '教训') specs = [!!entry.statement, !!(tags || entry.subject), /(避免|应该|下次|教训)/.test(body)];
|
|
5628
|
+
else if (cls === '待办') specs = [!!entry.statement, hasPriority, !!distillDateNear(sents.concat([String(entry.statement || '')]), body + ' ' + String(entry.statement || ''))];
|
|
5629
|
+
else if (cls === '成长') specs = [!!entry.statement, /(之前|后来|从.+到)/.test(body), /(效果|提升|%|更好)/.test(body)];
|
|
5630
|
+
else if (cls === '事件') specs = [!!(entry.created || entry.updated), !!(entry.subject || entry.statement), sents.length > 0];
|
|
5631
|
+
else if (cls === '规则边界') specs = [!!entry.statement, !!(tags || entry.subject), /(除非|例外)/.test(body)];
|
|
5632
|
+
else specs = [!!(entry.statement || body)];
|
|
5633
|
+
const detected = specs.filter(Boolean).length;
|
|
5634
|
+
return { detected: detected, expected: specs.length };
|
|
5635
|
+
}
|
|
5636
|
+
function distillAnalyze(root, entries, owner) {
|
|
5637
|
+
const classes = {};
|
|
5638
|
+
for (const name of DISTILL_CLASS_NAMES) classes[name] = { name: name, count: 0, items: [] };
|
|
5639
|
+
const skipped = [];
|
|
5640
|
+
let analyzed = 0, entriesWithItems = 0, itemCount = 0, traceOk = 0;
|
|
5641
|
+
let sourceChars = 0, extractedChars = 0, detected = 0, filled = 0;
|
|
5642
|
+
const refusedRe = /\uFFFD/;
|
|
5643
|
+
for (const e of entries) {
|
|
5644
|
+
const entryRoot = e._root || root;
|
|
5645
|
+
const fp = path.join(entryRoot, e.file);
|
|
5646
|
+
const entryOwner = ownerFromPrivatePath(entryRoot, fp) || owner;
|
|
5647
|
+
let raw;
|
|
5648
|
+
try {
|
|
5649
|
+
raw = readMemoryText(entryRoot, fp, entryOwner);
|
|
5650
|
+
} catch (err) {
|
|
5651
|
+
skipped.push({ file: e.file, reason: 'encoding', detail: String(err.message || err) });
|
|
5652
|
+
continue;
|
|
5653
|
+
}
|
|
5654
|
+
raw = String(raw).replace(/^\uFEFF/, '');
|
|
5655
|
+
if (!raw.trim()) {
|
|
5656
|
+
skipped.push({ file: e.file, reason: 'empty', detail: '' });
|
|
5657
|
+
continue;
|
|
5658
|
+
}
|
|
5659
|
+
if (refusedRe.test(raw)) {
|
|
5660
|
+
skipped.push({ file: e.file, reason: 'encoding', detail: 'UTF-8 解码失败' });
|
|
5661
|
+
continue;
|
|
5662
|
+
}
|
|
5663
|
+
const parsed = parseFrontmatter(raw);
|
|
5664
|
+
const hasMeta = !!(parsed.meta && parsed.meta.type);
|
|
5665
|
+
const local = Object.assign({}, e);
|
|
5666
|
+
if (hasMeta) {
|
|
5667
|
+
local.type = String(parsed.meta.type || e.type).toUpperCase();
|
|
5668
|
+
local.subject = parsed.meta.subject || e.subject;
|
|
5669
|
+
local.statement = parsed.meta.statement || e.statement;
|
|
5670
|
+
local.tags = parseTags(parsed.meta.tags);
|
|
5671
|
+
local.created = parsed.meta.created || e.created;
|
|
5672
|
+
local.updated = parsed.meta.updated || e.updated;
|
|
5673
|
+
} else {
|
|
5674
|
+
local.type = '';
|
|
5675
|
+
local.tags = [];
|
|
5676
|
+
local.subject = path.basename(e.file).replace(/\.md(\.enc)?$/, '');
|
|
5677
|
+
local.statement = String(parsed.body || raw).trim().split('\n')[0].slice(0, 200);
|
|
5678
|
+
skipped.push({ file: e.file, reason: 'no-structure', detail: '按裸文本处理', analyzed: true });
|
|
5679
|
+
}
|
|
5680
|
+
const cls = distillClassify(local, hasMeta ? parsed.meta : null);
|
|
5681
|
+
const body = String(parsed.body || '');
|
|
5682
|
+
const lines = raw.split('\n');
|
|
5683
|
+
const ex = distillExtract(local, cls, body, lines);
|
|
5684
|
+
const stats = distillElementStats(local, cls, body);
|
|
5685
|
+
detected += stats.detected;
|
|
5686
|
+
filled += ex.fieldNames.filter(function (n) { return ex.fields[n] !== '未记录'; }).length;
|
|
5687
|
+
const anchorText = '[溯源: ' + e.file + '#L' + ex.anchor.start + '-L' + ex.anchor.end + ']';
|
|
5688
|
+
const item = {
|
|
5689
|
+
ref: e.file,
|
|
5690
|
+
type: local.type || 'UNKNOWN',
|
|
5691
|
+
subject: local.subject,
|
|
5692
|
+
statement: local.statement,
|
|
5693
|
+
fields: ex.fields,
|
|
5694
|
+
anchor: e.file + '#L' + ex.anchor.start + '-L' + ex.anchor.end,
|
|
5695
|
+
anchorText: anchorText,
|
|
5696
|
+
truncated: ex.truncated,
|
|
5697
|
+
};
|
|
5698
|
+
classes[cls].items.push(item);
|
|
5699
|
+
classes[cls].count += 1;
|
|
5700
|
+
analyzed += 1;
|
|
5701
|
+
entriesWithItems += 1;
|
|
5702
|
+
itemCount += 1;
|
|
5703
|
+
traceOk += 1;
|
|
5704
|
+
sourceChars += String(local.statement || '').length + body.length;
|
|
5705
|
+
for (const n of ex.fieldNames) if (ex.fields[n] !== '未记录') extractedChars += String(ex.fields[n]).length;
|
|
5706
|
+
if (ex.truncated) skipped.push({ file: e.file, reason: 'truncated', detail: '超长字段已截断到 ' + DISTILL_FIELD_LIMIT + ' 字符', analyzed: true });
|
|
5707
|
+
}
|
|
5708
|
+
const mainLines = ['## 二、分类型提取清单', ''];
|
|
5709
|
+
if (!itemCount) mainLines.push('(无可提取条目)');
|
|
5710
|
+
for (const name of DISTILL_CLASS_NAMES) {
|
|
5711
|
+
const c = classes[name];
|
|
5712
|
+
if (!c.count) continue;
|
|
5713
|
+
mainLines.push('### ' + name + '(' + c.count + ' 条)');
|
|
5714
|
+
for (const item of c.items) {
|
|
5715
|
+
mainLines.push('- [' + item.type + '] ' + item.subject);
|
|
5716
|
+
for (const fn of Object.keys(item.fields)) mainLines.push(' - ' + fn + ': ' + item.fields[fn]);
|
|
5717
|
+
mainLines.push(' - ' + item.anchorText);
|
|
5718
|
+
mainLines.push('');
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
const compression = extractedChars > 0 ? Math.round((sourceChars / extractedChars) * 100) / 100 : 0;
|
|
5722
|
+
const entryCoverage = analyzed > 0 ? round4(entriesWithItems / analyzed) : 1;
|
|
5723
|
+
const traceCoverage = itemCount > 0 ? round4(traceOk / itemCount) : 1;
|
|
5724
|
+
const elementRate = detected > 0 ? round4(filled / detected) : 1;
|
|
5725
|
+
mainLines.push('## 三、质量指标', '');
|
|
5726
|
+
mainLines.push('- 源正文字符: ' + sourceChars + ' / 提取要点字符: ' + extractedChars + ' → 实测压缩比 ' + compression + '(只报本次实测,不承诺倍数)');
|
|
5727
|
+
mainLines.push('- 条目覆盖率: ' + Math.round(entryCoverage * 100) + '%(' + entriesWithItems + '/' + analyzed + ')');
|
|
5728
|
+
mainLines.push('- 溯源覆盖率: ' + Math.round(traceCoverage * 100) + '%(' + traceOk + '/' + itemCount + ',目标 100%)');
|
|
5729
|
+
mainLines.push('- 要素提取率: ' + Math.round(elementRate * 100) + '%(' + filled + '/' + detected + ',确定性回归护栏,不是语义保真承诺)');
|
|
5730
|
+
const tailLines = ['## 六、跳过与边界', ''];
|
|
5731
|
+
if (!skipped.length) tailLines.push('(无跳过项)');
|
|
5732
|
+
for (const s of skipped) {
|
|
5733
|
+
tailLines.push('- ' + s.file + ': ' + s.reason + (s.detail ? '(' + s.detail + ')' : '') + (s.analyzed ? '(已分析)' : '(已跳过)'));
|
|
5734
|
+
}
|
|
5735
|
+
return {
|
|
5736
|
+
classes: DISTILL_CLASS_NAMES.map(function (n) { return classes[n]; }).filter(function (c) { return c.count > 0; }),
|
|
5737
|
+
skipped: skipped,
|
|
5738
|
+
mainLines: mainLines,
|
|
5739
|
+
tailLines: tailLines,
|
|
5740
|
+
analyzed: analyzed,
|
|
5741
|
+
metrics: {
|
|
5742
|
+
compression_ratio: compression,
|
|
5743
|
+
entry_coverage: entryCoverage,
|
|
5744
|
+
trace_coverage: traceCoverage,
|
|
5745
|
+
element_extract_rate: elementRate,
|
|
5746
|
+
},
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
// 心理日志蒸馏:统计摘要 / 分类型提取 / 质量指标 / 主题画像 / 知识地图(启发式默认,可选 --model)
|
|
4936
5751
|
function distillCore(opts) {
|
|
4937
5752
|
opts = opts || {};
|
|
4938
5753
|
const root = userRoot();
|
|
@@ -4943,15 +5758,16 @@ function distillCore(opts) {
|
|
|
4943
5758
|
for (const r of memoryRoots()) {
|
|
4944
5759
|
for (const e of ensureIndex(r)) {
|
|
4945
5760
|
if (classifyRead(e, owner, '', !!opts.unsafe, selfAgent) === 'denied') continue;
|
|
4946
|
-
entries.push(e);
|
|
5761
|
+
entries.push(Object.assign({}, e, { _root: r }));
|
|
4947
5762
|
}
|
|
4948
5763
|
}
|
|
4949
5764
|
const lines = [];
|
|
4950
|
-
lines.push('# 元忆记忆蒸馏报告(
|
|
5765
|
+
lines.push('# 元忆记忆蒸馏报告(v' + VERSION + ')');
|
|
4951
5766
|
lines.push('');
|
|
4952
5767
|
lines.push('> 生成: yotta-memory distill | 启发式蒸馏(零依赖);--model <cmd> 可选外部模型增强');
|
|
4953
5768
|
lines.push('> 时间: ' + new Date().toISOString() + ' / 可读条目: ' + entries.length + ' 条');
|
|
4954
5769
|
lines.push('');
|
|
5770
|
+
const analysis = distillAnalyze(root, entries, owner);
|
|
4955
5771
|
// 1) 统计摘要
|
|
4956
5772
|
lines.push('## 一、统计摘要');
|
|
4957
5773
|
lines.push('');
|
|
@@ -4973,8 +5789,10 @@ function distillCore(opts) {
|
|
|
4973
5789
|
for (const e of entries) { const n = e.feedback_net || 0; fbSum += n; if (n > 0) usefulN++; else if (n < 0) uselessN++; }
|
|
4974
5790
|
lines.push('- 反馈统计: feedback_net 总和 ' + fbSum + '(正 ' + usefulN + ' 条 / 负 ' + uselessN + ' 条)');
|
|
4975
5791
|
lines.push('');
|
|
5792
|
+
lines.push.apply(lines, analysis.mainLines);
|
|
5793
|
+
lines.push('');
|
|
4976
5794
|
// 2) 主题画像(按 subject 聚类合并)
|
|
4977
|
-
lines.push('##
|
|
5795
|
+
lines.push('## 四、主题画像(按 subject 聚类,启发式合并)');
|
|
4978
5796
|
lines.push('');
|
|
4979
5797
|
const groups = {};
|
|
4980
5798
|
for (const e of entries) {
|
|
@@ -4993,7 +5811,7 @@ function distillCore(opts) {
|
|
|
4993
5811
|
lines.push('');
|
|
4994
5812
|
}
|
|
4995
5813
|
// 3) 知识地图(type + tags 层级)
|
|
4996
|
-
lines.push('##
|
|
5814
|
+
lines.push('## 五、知识地图(type → tags)');
|
|
4997
5815
|
lines.push('');
|
|
4998
5816
|
const map = {};
|
|
4999
5817
|
for (const e of entries) {
|
|
@@ -5008,6 +5826,8 @@ function distillCore(opts) {
|
|
|
5008
5826
|
lines.push('- ' + tk + ': ' + map[t][tk].map(function (e) { return e.subject; }).join(' / '));
|
|
5009
5827
|
}
|
|
5010
5828
|
}
|
|
5829
|
+
lines.push('');
|
|
5830
|
+
lines.push.apply(lines, analysis.tailLines);
|
|
5011
5831
|
let body = lines.join('\n') + '\n';
|
|
5012
5832
|
// 可选模型增强:--model <cmd>,stdin 传结构化摘要,stdout 取提炼文本
|
|
5013
5833
|
let modelOut = '';
|
|
@@ -5017,12 +5837,24 @@ function distillCore(opts) {
|
|
|
5017
5837
|
const r = runDistillModel(String(opts.model), payload);
|
|
5018
5838
|
if (r.stdout) modelOut = String(r.stdout).trim();
|
|
5019
5839
|
if (!modelOut) modelOut = '(模型无输出)';
|
|
5020
|
-
body += '\n##
|
|
5840
|
+
body += '\n## 七、模型提炼(--model ' + opts.model + ')\n\n' + modelOut + '\n';
|
|
5021
5841
|
} catch (e) {
|
|
5022
5842
|
modelOut = '(模型调用失败: ' + e.message + ')';
|
|
5023
|
-
body += '\n##
|
|
5843
|
+
body += '\n## 七、模型提炼\n\n' + modelOut + '\n';
|
|
5024
5844
|
}
|
|
5025
5845
|
}
|
|
5846
|
+
const makeReport = function (writtenPath) {
|
|
5847
|
+
return {
|
|
5848
|
+
schemaVersion: 1,
|
|
5849
|
+
generated: new Date().toISOString(),
|
|
5850
|
+
root: root,
|
|
5851
|
+
scope: { owner: owner, entries: entries.length, analyzed: analysis.analyzed },
|
|
5852
|
+
classes: analysis.classes,
|
|
5853
|
+
metrics: analysis.metrics,
|
|
5854
|
+
skipped: analysis.skipped,
|
|
5855
|
+
written: writtenPath || '',
|
|
5856
|
+
};
|
|
5857
|
+
};
|
|
5026
5858
|
// 落盘:--out 指定 / 私密 distills / 公共 facts/distills
|
|
5027
5859
|
let written = '';
|
|
5028
5860
|
try {
|
|
@@ -5053,9 +5885,9 @@ function distillCore(opts) {
|
|
|
5053
5885
|
}
|
|
5054
5886
|
}
|
|
5055
5887
|
} catch (e) {
|
|
5056
|
-
return { error: false, text: lines.join('\n') + '\n\n[落盘失败] ' + e.message };
|
|
5888
|
+
return { error: false, text: lines.join('\n') + '\n\n[落盘失败] ' + e.message, report: makeReport('') };
|
|
5057
5889
|
}
|
|
5058
|
-
return { error: false, text: body + '\n[已写入] ' + written };
|
|
5890
|
+
return { error: false, text: body + '\n[已写入] ' + written, report: makeReport(written) };
|
|
5059
5891
|
}
|
|
5060
5892
|
|
|
5061
5893
|
function promptPassword(promptText) {
|
|
@@ -6157,6 +6989,13 @@ function cmdExplain(ref, opts) {
|
|
|
6157
6989
|
if (r.error) process.exit(2);
|
|
6158
6990
|
}
|
|
6159
6991
|
function cmdMaintain(opts) {
|
|
6992
|
+
if (opts.capacity) {
|
|
6993
|
+
const r = capacityCore(opts);
|
|
6994
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
6995
|
+
else console.log(r.text);
|
|
6996
|
+
if (r.error) process.exit(2);
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6160
6999
|
if (opts.merge) {
|
|
6161
7000
|
const parts = String(opts.merge).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
6162
7001
|
if (parts.length < 2) { console.error('--merge 需要两个文件,逗号分隔'); process.exit(2); }
|
|
@@ -6171,7 +7010,8 @@ function cmdMaintain(opts) {
|
|
|
6171
7010
|
}
|
|
6172
7011
|
function cmdDistill(opts) {
|
|
6173
7012
|
const r = distillCore(opts);
|
|
6174
|
-
console.log(r.
|
|
7013
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
7014
|
+
else console.log(r.text);
|
|
6175
7015
|
if (r.error) process.exit(2);
|
|
6176
7016
|
}
|
|
6177
7017
|
function cmdForget(fileRef, opts) {
|
|
@@ -6205,7 +7045,9 @@ function cmdRename(fileRef, newName, opts) {
|
|
|
6205
7045
|
}
|
|
6206
7046
|
function cmdArchive(opts) {
|
|
6207
7047
|
const r = archiveCore(opts);
|
|
6208
|
-
console.log(r.
|
|
7048
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
7049
|
+
else console.log(r.text);
|
|
7050
|
+
if (r.error) process.exit(2);
|
|
6209
7051
|
}
|
|
6210
7052
|
function cmdBackupCreate(opts) {
|
|
6211
7053
|
const r = backupCreateCore(opts);
|
|
@@ -6723,6 +7565,7 @@ function contextCore(opts) {
|
|
|
6723
7565
|
opts = opts || {};
|
|
6724
7566
|
const roots = memoryRoots();
|
|
6725
7567
|
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
7568
|
+
if (opts.audit) return contextAuditCore(opts);
|
|
6726
7569
|
const root = userRoot();
|
|
6727
7570
|
const limit = parseInt(opts.limit || '10', 10) || 10;
|
|
6728
7571
|
const ident = resolveIdentity(opts);
|
|
@@ -6750,6 +7593,7 @@ function contextCore(opts) {
|
|
|
6750
7593
|
const trace = [];
|
|
6751
7594
|
const lines = [];
|
|
6752
7595
|
const shownFiles = new Set();
|
|
7596
|
+
const usedFiles = new Set();
|
|
6753
7597
|
const FENCE = String.fromCharCode(96, 96, 96);
|
|
6754
7598
|
function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
|
|
6755
7599
|
function appendContextEntry(entry, reason, options) {
|
|
@@ -6766,6 +7610,7 @@ function contextCore(opts) {
|
|
|
6766
7610
|
}
|
|
6767
7611
|
lines.push(line);
|
|
6768
7612
|
shownFiles.add(entry.file);
|
|
7613
|
+
usedFiles.add(entry.file);
|
|
6769
7614
|
trace.push('[included] ' + entry.file + ' reason: ' + reason);
|
|
6770
7615
|
return true;
|
|
6771
7616
|
}
|
|
@@ -6847,7 +7692,8 @@ function contextCore(opts) {
|
|
|
6847
7692
|
selfAgent: selfAgent,
|
|
6848
7693
|
years: years,
|
|
6849
7694
|
embedding: embeddingCommand,
|
|
6850
|
-
embeddingTimeout: embeddingTimeout
|
|
7695
|
+
embeddingTimeout: embeddingTimeout,
|
|
7696
|
+
noUsage: true
|
|
6851
7697
|
});
|
|
6852
7698
|
const focusedEntries = focused.entries || [];
|
|
6853
7699
|
if (!focusedEntries.length) lines.push('(无匹配记忆)');
|
|
@@ -6914,6 +7760,14 @@ function contextCore(opts) {
|
|
|
6914
7760
|
if (!trace.length) lines.push('(无选择记录)');
|
|
6915
7761
|
for (const t of trace) lines.push(t);
|
|
6916
7762
|
}
|
|
7763
|
+
// v0.18.0 A3:纳入上下文包的条目记 use 信号(本次调用内按文件去重;--no-usage 可关)。
|
|
7764
|
+
if (usedFiles.size) {
|
|
7765
|
+
const usageOpts = { selfAgent: selfAgent, noUsage: !!opts.noUsage };
|
|
7766
|
+
for (const r of roots) {
|
|
7767
|
+
const rels = Array.from(usedFiles).filter(function (rel) { return fs.existsSync(path.join(r, rel)); });
|
|
7768
|
+
if (rels.length) applyUsageHits(r, rels, 'use', '', usageOpts);
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
6917
7771
|
const reliability = doctorCore({ root: root });
|
|
6918
7772
|
if (reliability.level !== 'ok') {
|
|
6919
7773
|
lines.push('');
|
|
@@ -6923,11 +7777,12 @@ function contextCore(opts) {
|
|
|
6923
7777
|
for (const message of reliability.warnings) lines.push('- [警告] ' + message);
|
|
6924
7778
|
if (!reliability.ok) lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
|
|
6925
7779
|
}
|
|
6926
|
-
return { error: false, exitCode: 0, text: lines.join('\n') };
|
|
7780
|
+
return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace };
|
|
6927
7781
|
}
|
|
6928
7782
|
function cmdContext(opts) {
|
|
6929
7783
|
const r = contextCore(opts);
|
|
6930
|
-
console.log(r.
|
|
7784
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
7785
|
+
else console.log(r.text);
|
|
6931
7786
|
if (r.error) {
|
|
6932
7787
|
const reminder = migrationReminderText(userRoot());
|
|
6933
7788
|
if (reminder) console.log('\n' + reminder);
|
|
@@ -6935,6 +7790,221 @@ function cmdContext(opts) {
|
|
|
6935
7790
|
if (r.exitCode) process.exit(r.exitCode);
|
|
6936
7791
|
}
|
|
6937
7792
|
|
|
7793
|
+
// ---- v0.18.0 A7:context --audit 上下文压缩审计(只读;不自动补写)----
|
|
7794
|
+
function contextAuditGate(opts) {
|
|
7795
|
+
const raw = Array.isArray(opts.gate) ? opts.gate[opts.gate.length - 1] : opts.gate;
|
|
7796
|
+
if (raw === undefined || raw === null || raw === '') return null;
|
|
7797
|
+
const n = parseInt(raw, 10);
|
|
7798
|
+
return isNaN(n) ? null : n;
|
|
7799
|
+
}
|
|
7800
|
+
function contextAuditReadStdin() {
|
|
7801
|
+
// 只在 fd 0 明确是管道 / socket 时读取,避免测试运行器或交互终端被阻塞。
|
|
7802
|
+
try {
|
|
7803
|
+
const st = fs.fstatSync(0);
|
|
7804
|
+
if (!st.isFIFO() && !st.isSocket()) return null;
|
|
7805
|
+
return fs.readFileSync(0, 'utf8');
|
|
7806
|
+
} catch (e) {
|
|
7807
|
+
return null;
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7810
|
+
function contextAuditSegments(text) {
|
|
7811
|
+
const out = [];
|
|
7812
|
+
// 中文信号词为主;补英文决策词,保证中英混排的宿主压缩摘要也能切出候选句。
|
|
7813
|
+
const keywordRe = /决定|敲定|约定|口径|红线|规则|承诺|下一步|待办|遗留|注意|以后|必须|不要|decision|decided|agreement|policy|rule|commit|next step|todo|remaining|note|must|never/i;
|
|
7814
|
+
const sectionRe = /^#{1,6}\s*(决策|下一步|遗留问题|decisions?|next\s*steps?|remaining|todos?)/i;
|
|
7815
|
+
let inSection = false;
|
|
7816
|
+
let inCode = false;
|
|
7817
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
7818
|
+
const line = raw.trim();
|
|
7819
|
+
if (/^```/.test(line)) { inCode = !inCode; continue; }
|
|
7820
|
+
if (inCode || !line) continue;
|
|
7821
|
+
if (/^#{1,6}\s+/.test(line)) { inSection = sectionRe.test(line); continue; }
|
|
7822
|
+
if (/^\|/.test(line)) continue;
|
|
7823
|
+
const item = line.replace(/^[-*+]\s+/, '').replace(/^\d+[.、)]\s*/, '').trim();
|
|
7824
|
+
if (item.length < 4) continue;
|
|
7825
|
+
if (inSection || keywordRe.test(item)) {
|
|
7826
|
+
if (out.indexOf(item) === -1) out.push(item);
|
|
7827
|
+
}
|
|
7828
|
+
}
|
|
7829
|
+
return out;
|
|
7830
|
+
}
|
|
7831
|
+
function contextAuditCoverage(candidate, entries, minCoverage) {
|
|
7832
|
+
for (const e of entries) {
|
|
7833
|
+
const corpus = String(e.subject || '') + ' ' + String(e.statement || '');
|
|
7834
|
+
if (corpus.indexOf(candidate) !== -1) return { entry: e, coverage: 1, best: e };
|
|
7835
|
+
const stmt = String(e.statement || '');
|
|
7836
|
+
if (stmt.length >= 6 && candidate.indexOf(stmt) !== -1) return { entry: e, coverage: 1, best: e };
|
|
7837
|
+
}
|
|
7838
|
+
const tokens = new Set(tokenize(candidate).filter(function (t) { return t.indexOf(':') === -1; }));
|
|
7839
|
+
if (!tokens.size) return { entry: null, coverage: 0, best: null };
|
|
7840
|
+
let best = null;
|
|
7841
|
+
let bestCoverage = 0;
|
|
7842
|
+
for (const e of entries) {
|
|
7843
|
+
const etoks = new Set(tokenize(String(e.subject || '') + ' ' + String(e.statement || '')).filter(function (t) { return t.indexOf(':') === -1; }));
|
|
7844
|
+
let inter = 0;
|
|
7845
|
+
for (const t of tokens) if (etoks.has(t)) inter++;
|
|
7846
|
+
const coverage = inter / tokens.size;
|
|
7847
|
+
if (coverage > bestCoverage) { bestCoverage = coverage; best = e; }
|
|
7848
|
+
}
|
|
7849
|
+
return { entry: bestCoverage >= minCoverage ? best : null, coverage: bestCoverage, best: best };
|
|
7850
|
+
}
|
|
7851
|
+
function contextAuditRememberCommand(text) {
|
|
7852
|
+
const clean = String(text || '').replace(/"/g, "'").trim();
|
|
7853
|
+
const specificity = clean.replace(/^[^::]*[::]\s*/, '').trim();
|
|
7854
|
+
const subject = contextAuditSuggestionSubject(specificity || clean, 20);
|
|
7855
|
+
return 'yotta-memory remember FACT "' + subject + '" "' + clean + '" --verify';
|
|
7856
|
+
}
|
|
7857
|
+
function contextAuditSuggestionSubject(text, max) {
|
|
7858
|
+
// v0.18.0 修正:截断回退到非标点边界,避免切在「(」这类标点中间。
|
|
7859
|
+
const limit = parseInt(max, 10) || 20;
|
|
7860
|
+
const clean = String(text || '').trim();
|
|
7861
|
+
if (clean.length <= limit) return clean;
|
|
7862
|
+
const cut = clean.slice(0, limit);
|
|
7863
|
+
const trimmed = cut.replace(/[\s,。;:、,.;:!?!?()()【】\[\]「」『』“”'"—–\-]+$/, '');
|
|
7864
|
+
return trimmed.length >= 8 ? trimmed : cut;
|
|
7865
|
+
}
|
|
7866
|
+
function contextAuditCore(opts) {
|
|
7867
|
+
opts = opts || {};
|
|
7868
|
+
const roots = memoryRoots();
|
|
7869
|
+
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
7870
|
+
const ident = resolveIdentity(opts);
|
|
7871
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
7872
|
+
if (!ident.id) {
|
|
7873
|
+
return {
|
|
7874
|
+
error: true,
|
|
7875
|
+
exitCode: 3,
|
|
7876
|
+
text: 'context --audit 需要先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
|
|
7877
|
+
};
|
|
7878
|
+
}
|
|
7879
|
+
const selfAgent = ident.id;
|
|
7880
|
+
const owner = opts.owner || selfAgent;
|
|
7881
|
+
const root = userRoot();
|
|
7882
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
7883
|
+
if (keyError) return { error: true, exitCode: 3, text: keyError };
|
|
7884
|
+
const cfg = loadConfig();
|
|
7885
|
+
const rawMin = parseFloat(cfg.context_audit_min_coverage);
|
|
7886
|
+
const minCoverage = (rawMin > 0 && rawMin <= 1) ? rawMin : 0.5;
|
|
7887
|
+
let input = null;
|
|
7888
|
+
let source = '';
|
|
7889
|
+
if (opts.text !== undefined && opts.text !== null && String(opts.text) !== '') {
|
|
7890
|
+
// v0.18.0:内联文本输入(MCP 侧用它替代文件路径,避免把 --from 变成任意文件读取通道)。
|
|
7891
|
+
input = String(opts.text);
|
|
7892
|
+
source = 'inline';
|
|
7893
|
+
} else if (opts.from !== undefined && opts.from !== null && String(opts.from) !== '') {
|
|
7894
|
+
if (String(opts.from) === '-') {
|
|
7895
|
+
input = fs.readFileSync(0, 'utf8');
|
|
7896
|
+
source = 'stdin';
|
|
7897
|
+
} else {
|
|
7898
|
+
const fp = path.resolve(String(opts.from));
|
|
7899
|
+
if (!fs.existsSync(fp)) return { error: true, exitCode: 2, text: '输入文件不存在: ' + fp };
|
|
7900
|
+
input = fs.readFileSync(fp, 'utf8');
|
|
7901
|
+
source = fp;
|
|
7902
|
+
}
|
|
7903
|
+
} else {
|
|
7904
|
+
// noStdin:MCP(stdio)的 fd 0 是协议流,绝不能被当成待审计内容读走。
|
|
7905
|
+
const piped = opts.noStdin ? null : contextAuditReadStdin();
|
|
7906
|
+
if (piped !== null && piped.trim()) { input = piped; source = 'stdin'; }
|
|
7907
|
+
}
|
|
7908
|
+
const entries = [];
|
|
7909
|
+
for (const r of roots) {
|
|
7910
|
+
for (const e of indexEntriesFor(r, {}).entries) {
|
|
7911
|
+
if (classifyRead(e, owner, '', !!opts.unsafe, selfAgent) === 'denied') continue;
|
|
7912
|
+
entries.push(e);
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
const gate = contextAuditGate(opts);
|
|
7916
|
+
if (input === null) {
|
|
7917
|
+
const ctx = contextCore(Object.assign({}, opts, { audit: false, explain: true, noUsage: true }));
|
|
7918
|
+
const drops = (ctx.trace || []).filter(function (t) { return t.indexOf('[dropped]') === 0; });
|
|
7919
|
+
const items = drops.map(function (line) {
|
|
7920
|
+
const m = /^\[dropped\]\s+(\S+)\s+reason:\s+(\S+)/.exec(line);
|
|
7921
|
+
const file = m ? m[1] : '';
|
|
7922
|
+
const reason = m ? m[2] : '';
|
|
7923
|
+
const found = entries.filter(function (e) { return e.file === file; })[0];
|
|
7924
|
+
return {
|
|
7925
|
+
file: file,
|
|
7926
|
+
reason: reason,
|
|
7927
|
+
recallable: !!found,
|
|
7928
|
+
subject: found ? found.subject : '',
|
|
7929
|
+
};
|
|
7930
|
+
});
|
|
7931
|
+
const unrecallable = items.filter(function (x) { return !x.recallable; }).length;
|
|
7932
|
+
const lines = ['## yotta-memory context --audit(丢弃清单审计)', ''];
|
|
7933
|
+
lines.push('- 模式: 无输入内容,审计当前上下文包的丢弃清单(duplicate / budget_exceeded)');
|
|
7934
|
+
lines.push('- 计数: dropped ' + items.length + ' 条 / 仍可召回 ' + (items.length - unrecallable) + ' 条 / 无法召回 ' + unrecallable + ' 条');
|
|
7935
|
+
lines.push('');
|
|
7936
|
+
if (!items.length) lines.push('(本次上下文包无丢弃项。)');
|
|
7937
|
+
for (const item of items) {
|
|
7938
|
+
lines.push('- ' + item.file + '(' + item.reason + ')' + (item.recallable
|
|
7939
|
+
? ' → 仍可召回: yotta-memory recall "' + String(item.subject).replace(/"/g, "'") + '"'
|
|
7940
|
+
: ' → 未在当前可读索引中找到,请人工核对'));
|
|
7941
|
+
}
|
|
7942
|
+
if (gate !== null) lines.push('', '- --gate ' + gate + ': 无法召回条数 ' + unrecallable + '(超过门槛时 exit 1)');
|
|
7943
|
+
return {
|
|
7944
|
+
error: false,
|
|
7945
|
+
exitCode: (gate !== null && unrecallable > gate) ? 1 : 0,
|
|
7946
|
+
text: lines.join('\n'),
|
|
7947
|
+
report: {
|
|
7948
|
+
schemaVersion: 1,
|
|
7949
|
+
mode: 'dropped',
|
|
7950
|
+
summary: { dropped: items.length, recallable: items.length - unrecallable, unrecallable: unrecallable },
|
|
7951
|
+
items: items,
|
|
7952
|
+
},
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7955
|
+
const segments = contextAuditSegments(input);
|
|
7956
|
+
const items = segments.map(function (text) {
|
|
7957
|
+
const cov = contextAuditCoverage(text, entries, minCoverage);
|
|
7958
|
+
let status = 'missing';
|
|
7959
|
+
let matched = null;
|
|
7960
|
+
if (cov.entry) { status = 'landed'; matched = cov.entry; }
|
|
7961
|
+
else if (cov.best && cov.coverage > 0 && cov.coverage >= minCoverage * 0.6) status = 'uncertain';
|
|
7962
|
+
return {
|
|
7963
|
+
text: text,
|
|
7964
|
+
status: status,
|
|
7965
|
+
coverage: round3(cov.coverage),
|
|
7966
|
+
matched_file: matched ? matched.file : '',
|
|
7967
|
+
matched_subject: matched ? matched.subject : '',
|
|
7968
|
+
suggested_command: status === 'missing' ? contextAuditRememberCommand(text) : '',
|
|
7969
|
+
};
|
|
7970
|
+
});
|
|
7971
|
+
const landed = items.filter(function (x) { return x.status === 'landed'; });
|
|
7972
|
+
const missing = items.filter(function (x) { return x.status === 'missing'; });
|
|
7973
|
+
const uncertain = items.filter(function (x) { return x.status === 'uncertain'; });
|
|
7974
|
+
const lines = ['## yotta-memory context --audit(上下文压缩审计)', ''];
|
|
7975
|
+
lines.push('- 来源: ' + source + '(只读;不会自动补写记忆)');
|
|
7976
|
+
lines.push('- 汇总: 候选 ' + items.length + ' 条 / 已落盘 ' + landed.length + ' 条 / 未落盘 ' + missing.length + ' 条 / 无法判定 ' + uncertain.length + ' 条');
|
|
7977
|
+
lines.push('');
|
|
7978
|
+
lines.push('### 未落盘(建议落盘,命令仅打印不执行)');
|
|
7979
|
+
if (!missing.length) lines.push('(无)');
|
|
7980
|
+
for (const item of missing) {
|
|
7981
|
+
lines.push('- ' + item.text.slice(0, 120));
|
|
7982
|
+
lines.push(' → ' + item.suggested_command);
|
|
7983
|
+
}
|
|
7984
|
+
lines.push('');
|
|
7985
|
+
lines.push('### 无法判定(人工核对)');
|
|
7986
|
+
if (!uncertain.length) lines.push('(无)');
|
|
7987
|
+
for (const item of uncertain) lines.push('- ' + item.text.slice(0, 120) + '(覆盖率 ' + item.coverage + ')');
|
|
7988
|
+
lines.push('');
|
|
7989
|
+
lines.push('### 已落盘');
|
|
7990
|
+
if (!landed.length) lines.push('(无)');
|
|
7991
|
+
for (const item of landed) lines.push('- ' + item.text.slice(0, 120) + ' → ' + item.matched_file);
|
|
7992
|
+
if (gate !== null) lines.push('', '- --gate ' + gate + ': 未落盘 ' + missing.length + ' 条(超过门槛时 exit 1)');
|
|
7993
|
+
return {
|
|
7994
|
+
error: false,
|
|
7995
|
+
exitCode: (gate !== null && missing.length > gate) ? 1 : 0,
|
|
7996
|
+
text: lines.join('\n'),
|
|
7997
|
+
report: {
|
|
7998
|
+
schemaVersion: 1,
|
|
7999
|
+
mode: 'input',
|
|
8000
|
+
source: source,
|
|
8001
|
+
thresholds: { min_coverage: minCoverage },
|
|
8002
|
+
summary: { candidates: items.length, landed: landed.length, missing: missing.length, uncertain: uncertain.length },
|
|
8003
|
+
items: items,
|
|
8004
|
+
},
|
|
8005
|
+
};
|
|
8006
|
+
}
|
|
8007
|
+
|
|
6938
8008
|
// ---- v0.17.0 A1: bench 可复算基准评测(只读 / 本地 / 零依赖)----
|
|
6939
8009
|
const BENCH_SCHEMA_VERSION = 1;
|
|
6940
8010
|
const BENCH_DEFAULT_SEED = 20260925;
|
|
@@ -8023,17 +9093,20 @@ async function cmdScan(opts) {
|
|
|
8023
9093
|
|
|
8024
9094
|
// ---- config 命令 ----
|
|
8025
9095
|
// v0.10.0:maintain_* / consolidate_* 数值键纳入 config set/get(此前只支持 3 个键但文档已写可调)
|
|
8026
|
-
function isNumericConfigKey(key) { return /^(maintain_|consolidate_|scale_|backup_max_age_hours$)/.test(key); }
|
|
9096
|
+
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); }
|
|
8027
9097
|
// v0.17.0 B2:doctor 规模体检阈值键(0 表示只要超过 0 就告警,便于压测与演练)
|
|
8028
|
-
const SCALE_CONFIG_KEYS = ['scale_warn_entries', 'scale_warn_files_per_dir', 'scale_warn_index_bytes', 'scale_warn_cold_start_ms'];
|
|
9098
|
+
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'];
|
|
9099
|
+
// v0.18.1 验收修正:config set / get 共用一份键列表,新增配置键不会再出现「读端认、写端拒」。
|
|
9100
|
+
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);
|
|
8029
9101
|
function cmdConfigSet(key, value) {
|
|
8030
|
-
const known =
|
|
8031
|
-
if (known.indexOf(key) === -1) { console.error('未知配置项: ' + key + '(可用: memory_home / backup_dir / embedding_cmd / embedding_timeout / maintain_* / consolidate_* / scale_*)'); process.exit(2); }
|
|
9102
|
+
const known = CONFIG_VALUE_KEYS;
|
|
9103
|
+
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); }
|
|
8032
9104
|
if (value === undefined || value === null || value === '') { console.error('缺少值: config set ' + key + ' <值>'); process.exit(2); }
|
|
8033
9105
|
const cfg = loadConfig();
|
|
8034
9106
|
if (key === 'memory_home') cfg.memory_home = value;
|
|
8035
9107
|
else if (key === 'backup_dir') cfg.backup_dir = value;
|
|
8036
9108
|
else if (key === 'backup_enabled') cfg.backup_enabled = value !== 'false' && value !== '0';
|
|
9109
|
+
else if (key === 'usage_enabled') cfg.usage_enabled = value !== 'false' && value !== '0';
|
|
8037
9110
|
else if (key === 'backup_schedule' || key === 'backup_time' || key === 'backup_setup_choice') cfg[key] = value;
|
|
8038
9111
|
else if (key === 'embedding_cmd') cfg.embedding_cmd = value;
|
|
8039
9112
|
else if (key === 'embedding_timeout') cfg.embedding_timeout = parseInt(value, 10) || 3000;
|
|
@@ -8059,11 +9132,12 @@ function cmdConfigGet(opts) {
|
|
|
8059
9132
|
console.log('backup_enabled: ' + (cfg.backup_enabled === undefined ? '(未设置)' : cfg.backup_enabled));
|
|
8060
9133
|
console.log('backup_schedule: ' + (cfg.backup_schedule || '(未设置)'));
|
|
8061
9134
|
console.log('backup_time: ' + (cfg.backup_time || '(未设置)'));
|
|
9135
|
+
console.log('usage_enabled: ' + (cfg.usage_enabled === undefined ? '(默认 true)' : cfg.usage_enabled));
|
|
8062
9136
|
console.log('embedding_cmd: ' + (cfg.embedding_cmd || '(未设置)'));
|
|
8063
9137
|
console.log('embedding_timeout: ' + (cfg.embedding_timeout || 3000));
|
|
8064
|
-
const keys =
|
|
9138
|
+
const keys = CONFIG_VALUE_KEYS;
|
|
8065
9139
|
for (const k of keys) {
|
|
8066
|
-
if (k === 'memory_home' || k === 'backup_dir' || k === 'backup_enabled' || k === 'backup_schedule' || k === 'backup_time' || k === 'embedding_cmd' || k === 'embedding_timeout') continue;
|
|
9140
|
+
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;
|
|
8067
9141
|
if (cfg[k] !== undefined) console.log(k + ': ' + cfg[k]);
|
|
8068
9142
|
}
|
|
8069
9143
|
console.log('当前生效用户级位置: ' + userRoot());
|
|
@@ -8080,17 +9154,18 @@ function mcpTools(profile) {
|
|
|
8080
9154
|
{ name: 'remember', description: '写入一条记忆。参数 type(FACT/PREF/BOUND/COMMIT)、subject、statement 必填;owner 可选,默认当前智能体', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'FACT/PREF/BOUND/COMMIT' }, subject: { type: 'string' }, statement: { type: 'string' }, owner: { type: 'string' } }, required: ['type', 'subject', 'statement'] } },
|
|
8081
9155
|
{ name: 'recall', description: '检索记忆。query 可选;type 可选;limit 可选(默认 20);explain 可选。只返回当前智能体可读记忆;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
|
|
8082
9156
|
{ name: 'search', description: '检索记忆(同 recall)。query 可选;type 可选;limit 可选(默认 20);explain 可选;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
|
|
8083
|
-
{ name: 'context', description: '生成开工上下文包。focus 可选;limit 可选;budget 可选;explain 可选;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { focus: { type: 'string' }, limit: { type: 'number' }, budget: { type: 'number' }, explain: { type: 'boolean' }, embeddingTimeout: { type: 'number' } } } },
|
|
9157
|
+
{ name: 'context', description: '生成开工上下文包。focus 可选;limit 可选;budget 可选;explain 可选;audit=true 时改为上下文压缩审计(用 auditText 传待审计文本,只读),gate 控制未落盘条数门槛;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { focus: { type: 'string' }, limit: { type: 'number' }, budget: { type: 'number' }, explain: { type: 'boolean' }, embeddingTimeout: { type: 'number' }, audit: { type: 'boolean' }, auditText: { type: 'string' }, gate: { type: 'number' } } } },
|
|
8084
9158
|
{ name: 'doctor', description: '开工可靠性检查:检查记忆库根目录、密钥库、索引、身份登记与最近备份;只读,不会修改记忆', inputSchema: { type: 'object', properties: {} } },
|
|
8085
9159
|
{ name: 'forget', description: '删除一条记忆。file 为记忆文件路径(如 facts/2026-08-24-0001.md 或文件名)', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
8086
|
-
{ name: 'archive', description: '归档旧记忆。days 默认 180;threshold 默认 0.4', inputSchema: { type: 'object', properties: { days: { type: 'number' }, threshold: { type: 'number' } } } },
|
|
9160
|
+
{ name: 'archive', description: '归档旧记忆。days 默认 180;threshold 默认 0.4;dryRun=true 只预览、不落盘。破坏性覆盖 --force 仅本机 CLI 提供(MCP 不接受)', inputSchema: { type: 'object', properties: { days: { type: 'number' }, threshold: { type: 'number' }, dryRun: { type: 'boolean' } } } },
|
|
8087
9161
|
{ name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
|
|
8088
9162
|
{ name: 'export', description: '导出全部记忆到记忆库内的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json;仅限记忆库内路径)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
|
|
8089
9163
|
{ name: 'import', description: '从记忆库内的 JSON 文件导入记忆。src 为文件路径(相对记忆库目录,或记忆库内绝对路径;仅限记忆库内)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
|
|
8090
9164
|
{ name: 'agent_info', description: '查看当前智能体身份与登记状态(HTTP 读经 token 校验的 X-Agent-Id;stdio 读 --agent-id 显式参数)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
|
|
8091
9165
|
{ name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
|
|
8092
9166
|
{ 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'] } },
|
|
8093
|
-
{ name: 'maintain', description: '记忆自组织(自我进化):规则层归档/遗忘/去重预览。默认 dry-run;apply 才执行,purge
|
|
9167
|
+
{ 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' } } } },
|
|
9168
|
+
{ 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' } } } },
|
|
8094
9169
|
{ name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体(MCP 不支持 --model 外部命令)', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' } } } },
|
|
8095
9170
|
{ name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
|
|
8096
9171
|
];
|
|
@@ -8132,6 +9207,16 @@ function callToolInner(name, args, ctx) {
|
|
|
8132
9207
|
return { text: r.text, error: r.error };
|
|
8133
9208
|
}
|
|
8134
9209
|
if (name === 'context') {
|
|
9210
|
+
if (args.audit) {
|
|
9211
|
+
// v0.18.0 A7:MCP 侧只接受内联文本(不接受文件路径,避免把 --from 变成任意文件读取通道),
|
|
9212
|
+
// 也不读 stdin(stdio MCP 的 fd 0 是协议流)。
|
|
9213
|
+
const auditText = (args.auditText !== undefined && args.auditText !== null) ? String(args.auditText) : '';
|
|
9214
|
+
if (!auditText.trim()) {
|
|
9215
|
+
return { text: '拒绝: MCP 的 context audit 只接受内联文本(auditText)。审计本地文件或标准输入请在本机 CLI 运行 yotta-memory context --audit --from <文件|->。', error: true };
|
|
9216
|
+
}
|
|
9217
|
+
const r = contextAuditCore({ selfAgent: agent, owner: agent, text: auditText, gate: args.gate, noStdin: true });
|
|
9218
|
+
return { text: r.text, error: r.error };
|
|
9219
|
+
}
|
|
8135
9220
|
const r = contextCore({
|
|
8136
9221
|
focus: args.focus ? String(args.focus) : '',
|
|
8137
9222
|
limit: args.limit || 10,
|
|
@@ -8151,7 +9236,7 @@ function callToolInner(name, args, ctx) {
|
|
|
8151
9236
|
return { text: r.text, error: r.error };
|
|
8152
9237
|
}
|
|
8153
9238
|
if (name === 'archive') {
|
|
8154
|
-
const r = archiveCore({ selfAgent: agent, days: args.days, threshold: args.threshold });
|
|
9239
|
+
const r = archiveCore({ selfAgent: agent, days: args.days, threshold: args.threshold, dryRun: !!args.dryRun });
|
|
8155
9240
|
return { text: r.text, error: r.error };
|
|
8156
9241
|
}
|
|
8157
9242
|
if (name === 'reindex') {
|
|
@@ -8208,7 +9293,17 @@ function callToolInner(name, args, ctx) {
|
|
|
8208
9293
|
return { text: r.text, error: r.error };
|
|
8209
9294
|
}
|
|
8210
9295
|
if (name === 'maintain') {
|
|
8211
|
-
const r =
|
|
9296
|
+
const r = args.capacity
|
|
9297
|
+
? capacityCore({ selfAgent: agent, limit: args.limit })
|
|
9298
|
+
: maintainCore({ selfAgent: agent, apply: !!args.apply, purge: !!args.purge, dedup: !!args.dedup, rules: !!args.rules, threshold: args.threshold, age: args.age });
|
|
9299
|
+
return { text: r.text, error: r.error };
|
|
9300
|
+
}
|
|
9301
|
+
if (name === 'consolidate') {
|
|
9302
|
+
// v0.18.0 A5:MCP 只读 propose;apply / undo / batches 留在本机 CLI(由用户确认后执行)。
|
|
9303
|
+
if (args.apply || args.undo || args.batches || args.yes) {
|
|
9304
|
+
return { text: '拒绝: MCP 的 consolidate 只提供只读 propose 报告;--apply / --undo / --batches 请在本机 CLI 由用户确认后运行(AI 不得代替用户执行 --apply)。', error: true };
|
|
9305
|
+
}
|
|
9306
|
+
const r = consolidateCore({ selfAgent: agent, apply: false, minAge: args.minAge, minIdle: args.minIdle, maxUtility: args.maxUtility, minGroup: args.minGroup, period: args.period, type: args.type });
|
|
8212
9307
|
return { text: r.text, error: r.error };
|
|
8213
9308
|
}
|
|
8214
9309
|
if (name === 'distill') {
|
|
@@ -9040,6 +10135,14 @@ function consolidateUndoCore(batch, opts) {
|
|
|
9040
10135
|
try {
|
|
9041
10136
|
if (fs.existsSync(to)) fs.unlinkSync(to);
|
|
9042
10137
|
if (fs.existsSync(from)) {
|
|
10138
|
+
const archiveOwner = archiveOwnerFromPath(x.root, from);
|
|
10139
|
+
try {
|
|
10140
|
+
const marked = readMemoryText(x.root, from, archiveOwner);
|
|
10141
|
+
const clean = stripConsolidateMarker(marked, x.rec.had_trailing_newline !== false);
|
|
10142
|
+
if (clean !== marked) writeMemoryText(x.root, from, clean, archiveOwner);
|
|
10143
|
+
} catch (eMarker) {
|
|
10144
|
+
err = err || ('巩固标记剥离失败: ' + x.rec.file + ' ' + eMarker.message);
|
|
10145
|
+
}
|
|
9043
10146
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
9044
10147
|
fs.renameSync(from, to);
|
|
9045
10148
|
upsertIndexEntry(x.root, readEntry(to, x.root));
|
|
@@ -9103,7 +10206,102 @@ function clusterConsolidateCandidates(cands) {
|
|
|
9103
10206
|
return groups;
|
|
9104
10207
|
}
|
|
9105
10208
|
|
|
9106
|
-
|
|
10209
|
+
// ---- v0.18.1 A9:相对日期绝对化 + 归档巩固标记 ----
|
|
10210
|
+
const CONSOLIDATE_MARKER_PREFIX = '<!-- yotta-memory: consolidated to ';
|
|
10211
|
+
const CONSOLIDATE_MARKER_RE = /<!-- yotta-memory: consolidated to [^\n]* -->\n?/;
|
|
10212
|
+
function ymdPad(n) { return String(n).padStart(2, '0'); }
|
|
10213
|
+
function ymdParse(s) {
|
|
10214
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || ''));
|
|
10215
|
+
if (!m) return null;
|
|
10216
|
+
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
10217
|
+
if (d.getFullYear() !== Number(m[1]) || d.getMonth() !== Number(m[2]) - 1 || d.getDate() !== Number(m[3])) return null;
|
|
10218
|
+
return d;
|
|
10219
|
+
}
|
|
10220
|
+
function ymdFormat(d) { return d.getFullYear() + '-' + ymdPad(d.getMonth() + 1) + '-' + ymdPad(d.getDate()); }
|
|
10221
|
+
function ymdAdd(s, days) { const d = ymdParse(s); if (!d) return ''; d.setDate(d.getDate() + days); return ymdFormat(d); }
|
|
10222
|
+
function ymdWeekRange(s) {
|
|
10223
|
+
const d = ymdParse(s);
|
|
10224
|
+
if (!d) return '';
|
|
10225
|
+
const mondayOffset = (d.getDay() + 6) % 7;
|
|
10226
|
+
const mon = new Date(d.getFullYear(), d.getMonth(), d.getDate() - mondayOffset);
|
|
10227
|
+
const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6);
|
|
10228
|
+
return ymdFormat(mon) + ' ~ ' + ymdFormat(sun);
|
|
10229
|
+
}
|
|
10230
|
+
function ymdMonthRange(s, offset) {
|
|
10231
|
+
const d = ymdParse(s);
|
|
10232
|
+
if (!d) return '';
|
|
10233
|
+
const first = new Date(d.getFullYear(), d.getMonth() + (offset || 0), 1);
|
|
10234
|
+
const last = new Date(first.getFullYear(), first.getMonth() + 1, 0);
|
|
10235
|
+
return ymdFormat(first) + ' ~ ' + ymdFormat(last);
|
|
10236
|
+
}
|
|
10237
|
+
function ymdYearRange(s, offset) {
|
|
10238
|
+
const d = ymdParse(s);
|
|
10239
|
+
if (!d) return '';
|
|
10240
|
+
const y = d.getFullYear() + (offset || 0);
|
|
10241
|
+
return y + '-01-01 ~ ' + y + '-12-31';
|
|
10242
|
+
}
|
|
10243
|
+
const RELATIVE_DATE_MAP = {
|
|
10244
|
+
'大前天': function (b) { return ymdAdd(b, -3); },
|
|
10245
|
+
'上上周': function (b) { return ymdWeekRange(ymdAdd(b, -7)); },
|
|
10246
|
+
'上个月': function (b) { return ymdMonthRange(b, -1); },
|
|
10247
|
+
'这个月': function (b) { return ymdMonthRange(b, 0); },
|
|
10248
|
+
'前天': function (b) { return ymdAdd(b, -2); },
|
|
10249
|
+
'昨天': function (b) { return ymdAdd(b, -1); },
|
|
10250
|
+
'昨日': function (b) { return ymdAdd(b, -1); },
|
|
10251
|
+
'今天': function (b) { return ymdAdd(b, 0); },
|
|
10252
|
+
'今日': function (b) { return ymdAdd(b, 0); },
|
|
10253
|
+
'明天': function (b) { return ymdAdd(b, 1); },
|
|
10254
|
+
'后天': function (b) { return ymdAdd(b, 2); },
|
|
10255
|
+
'上周': function (b) { return ymdWeekRange(ymdAdd(b, -7)); },
|
|
10256
|
+
'本周': function (b) { return ymdWeekRange(b); },
|
|
10257
|
+
'这周': function (b) { return ymdWeekRange(b); },
|
|
10258
|
+
'上月': function (b) { return ymdMonthRange(b, -1); },
|
|
10259
|
+
'本月': function (b) { return ymdMonthRange(b, 0); },
|
|
10260
|
+
'今年': function (b) { return ymdYearRange(b, 0); },
|
|
10261
|
+
'去年': function (b) { return ymdYearRange(b, -1); },
|
|
10262
|
+
};
|
|
10263
|
+
const RELATIVE_DATE_RE = /(大前天|上上周|上个月|这个月|前天|昨天|昨日|今天|今日|明天|后天|上周|本周|这周|上月|本月|今年|去年)(?![((]\d{4}-\d{2}-\d{2})/g;
|
|
10264
|
+
function absolutizeRelativeDates(text, baseYmd) {
|
|
10265
|
+
const s = String(text || '');
|
|
10266
|
+
if (!ymdParse(baseYmd)) return { text: s, count: 0 };
|
|
10267
|
+
let count = 0;
|
|
10268
|
+
const out = s.replace(RELATIVE_DATE_RE, function (word) {
|
|
10269
|
+
const fn = RELATIVE_DATE_MAP[word];
|
|
10270
|
+
if (!fn) return word;
|
|
10271
|
+
const rep = fn(baseYmd);
|
|
10272
|
+
if (!rep) return word;
|
|
10273
|
+
count += 1;
|
|
10274
|
+
return word + '(' + rep + ')';
|
|
10275
|
+
});
|
|
10276
|
+
return { text: out, count: count };
|
|
10277
|
+
}
|
|
10278
|
+
function consolidateMarkerText(summaryRel, batch) {
|
|
10279
|
+
return CONSOLIDATE_MARKER_PREFIX + summaryRel + ' | batch ' + batch + ' | ' + today() + ' -->';
|
|
10280
|
+
}
|
|
10281
|
+
function archiveOwnerFromPath(root, fp) {
|
|
10282
|
+
const seg = path.relative(root, fp).split(/[\\/]/);
|
|
10283
|
+
if (seg[0] === ARCHIVE_DIR && seg[1] === PRIVATE_DIR && seg[2]) return seg[2];
|
|
10284
|
+
return ownerFromPrivatePath(root, fp);
|
|
10285
|
+
}
|
|
10286
|
+
function appendConsolidateMarker(root, fp, summaryRel, batch, owner, opts) {
|
|
10287
|
+
if (opts && opts.markerFailForTest) throw new Error('marker fail for test');
|
|
10288
|
+
let text = readMemoryText(root, fp, owner);
|
|
10289
|
+
if (text.indexOf(CONSOLIDATE_MARKER_PREFIX) !== -1) return { appended: false, hadTrailingNewline: text.endsWith('\n') };
|
|
10290
|
+
const hadTrailingNewline = text.endsWith('\n');
|
|
10291
|
+
if (!hadTrailingNewline) text += '\n';
|
|
10292
|
+
text += consolidateMarkerText(summaryRel, batch) + '\n';
|
|
10293
|
+
writeMemoryText(root, fp, text, owner);
|
|
10294
|
+
return { appended: true, hadTrailingNewline: hadTrailingNewline };
|
|
10295
|
+
}
|
|
10296
|
+
function stripConsolidateMarker(text, hadTrailingNewline) {
|
|
10297
|
+
let out = String(text || '');
|
|
10298
|
+
if (out.indexOf(CONSOLIDATE_MARKER_PREFIX) === -1) return out;
|
|
10299
|
+
out = out.replace(CONSOLIDATE_MARKER_RE, '');
|
|
10300
|
+
if (hadTrailingNewline === false && out.endsWith('\n')) out = out.slice(0, -1);
|
|
10301
|
+
return out;
|
|
10302
|
+
}
|
|
10303
|
+
|
|
10304
|
+
function applyConsolidateGroup(root, group, batch, period, opts) {
|
|
9107
10305
|
const e0 = group[0];
|
|
9108
10306
|
const type = e0.type;
|
|
9109
10307
|
const owner = e0.scope === 'private' ? e0.owner : '';
|
|
@@ -9127,7 +10325,12 @@ function applyConsolidateGroup(root, group, batch, period) {
|
|
|
9127
10325
|
const avgU = round3(group.reduce(function (s, c) { return s + c.utility; }, 0) / group.length);
|
|
9128
10326
|
const bodyLines = ['# 周期摘要:' + theme, '', '> 生成: yotta-memory consolidate | 类型 ' + type + ' | 范围 ' + createdDates[0] + ' ~ ' + createdDates[createdDates.length - 1] + ' | ' + group.length + ' 条 | 平均效用 ' + avgU, '', '## 主题要点', ''];
|
|
9129
10327
|
const sorted = group.slice().sort(function (a, b) { return String(b.e.updated || '').localeCompare(String(a.e.updated || '')); });
|
|
9130
|
-
|
|
10328
|
+
let dateNormalized = 0;
|
|
10329
|
+
for (const c of sorted) {
|
|
10330
|
+
const norm = absolutizeRelativeDates(c.e.statement || '', c.e.created || c.e.updated || '');
|
|
10331
|
+
dateNormalized += norm.count;
|
|
10332
|
+
bodyLines.push('- [' + c.e.type + '] ' + (c.e.subject || '') + ': ' + norm.text);
|
|
10333
|
+
}
|
|
9131
10334
|
bodyLines.push('', '## 溯源(原文已入 .archive,可 consolidate --undo <batch> 恢复)', '');
|
|
9132
10335
|
for (const c of group) bodyLines.push('- ' + c.e.file + '(created ' + c.e.created + ' / confidence ' + c.e.confidence + ' / utility ' + round3(c.utility) + ')');
|
|
9133
10336
|
const body = bodyLines.join('\n');
|
|
@@ -9138,16 +10341,57 @@ function applyConsolidateGroup(root, group, batch, period) {
|
|
|
9138
10341
|
upsertIndexEntry(root, readEntry(fp, root));
|
|
9139
10342
|
appendAudit(root, 'audit', { action: 'summary_create', ts: new Date().toISOString(), batch: batch, file: summaryRel, sourceFiles: group.map(function (c) { return c.e.file; }) });
|
|
9140
10343
|
let moved = 0;
|
|
10344
|
+
let markersWritten = 0;
|
|
10345
|
+
let markerErrors = 0;
|
|
9141
10346
|
for (const c of group) {
|
|
9142
10347
|
const destDir = archiveDirFor(root, type, owner);
|
|
9143
10348
|
fs.mkdirSync(destDir, { recursive: true });
|
|
9144
10349
|
const dest = path.join(destDir, path.basename(c.e.file));
|
|
9145
10350
|
fs.renameSync(path.join(root, c.e.file), dest);
|
|
10351
|
+
let marker = { appended: false, hadTrailingNewline: true };
|
|
10352
|
+
try {
|
|
10353
|
+
marker = appendConsolidateMarker(root, dest, summaryRel, batch, owner, opts);
|
|
10354
|
+
if (marker.appended) markersWritten += 1;
|
|
10355
|
+
} catch (e) {
|
|
10356
|
+
markerErrors += 1;
|
|
10357
|
+
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) });
|
|
10358
|
+
}
|
|
9146
10359
|
removeIndexEntry(root, c.e.file);
|
|
9147
|
-
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) });
|
|
10360
|
+
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 });
|
|
9148
10361
|
moved++;
|
|
9149
10362
|
}
|
|
9150
|
-
return { made: 1, moved: moved, summaryRel: summaryRel };
|
|
10363
|
+
return { made: 1, moved: moved, summaryRel: summaryRel, dateNormalized: dateNormalized, markersWritten: markersWritten, markerErrors: markerErrors };
|
|
10364
|
+
}
|
|
10365
|
+
|
|
10366
|
+
// v0.18.0 A5:首次启用说明按 .archive 审计判定(不新建标记文件)。
|
|
10367
|
+
function consolidateHasManifest(root) {
|
|
10368
|
+
const dir = path.join(root, ARCHIVE_DIR);
|
|
10369
|
+
let names;
|
|
10370
|
+
try { names = fs.readdirSync(dir); } catch (e) { return false; }
|
|
10371
|
+
for (const name of names) {
|
|
10372
|
+
if (!/^audit-\d{4}-\d{2}-\d{2}\.jsonl$/.test(name)) continue;
|
|
10373
|
+
let raw;
|
|
10374
|
+
try { raw = fs.readFileSync(path.join(dir, name), 'utf8'); } catch (e) { continue; }
|
|
10375
|
+
for (const line of raw.split('\n')) {
|
|
10376
|
+
if (!line.trim()) continue;
|
|
10377
|
+
let rec;
|
|
10378
|
+
try { rec = JSON.parse(line); } catch (e) { continue; }
|
|
10379
|
+
if (rec && rec.action === 'manifest' && rec.command === 'consolidate') return true;
|
|
10380
|
+
}
|
|
10381
|
+
}
|
|
10382
|
+
return false;
|
|
10383
|
+
}
|
|
10384
|
+
function consolidateFirstRunNotice() {
|
|
10385
|
+
return [
|
|
10386
|
+
'### 首次启用说明(只显示一次)',
|
|
10387
|
+
'- 保留期:原文移入 .archive/ 后一直保留,直到你显式 purge 或 undo;不会自动删除。',
|
|
10388
|
+
'- 检查:yotta-memory consolidate(默认 --propose,只出报告不写盘)。',
|
|
10389
|
+
'- 纠正:yotta-memory consolidate --undo <batch>。',
|
|
10390
|
+
'- 导出:yotta-memory export。',
|
|
10391
|
+
'- 停用:不再执行 --apply,或用 yotta-memory config set consolidate_min_age 99999 抬高门槛。',
|
|
10392
|
+
'- 删除:yotta-memory maintain --purge(对遗忘候选真删)。',
|
|
10393
|
+
'- AI 不得代替用户执行 --apply;--apply 需要确认,非交互必须显式加 --yes。',
|
|
10394
|
+
];
|
|
9151
10395
|
}
|
|
9152
10396
|
|
|
9153
10397
|
function consolidateCore(opts) {
|
|
@@ -9157,6 +10401,15 @@ function consolidateCore(opts) {
|
|
|
9157
10401
|
const root = userRoot();
|
|
9158
10402
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
9159
10403
|
const apply = !!opts.apply;
|
|
10404
|
+
// v0.18.0 A5:--apply 是破坏性写入;非交互必须显式 --yes,交互确认在 cmdConsolidate 完成。
|
|
10405
|
+
if (apply && !opts.yes) {
|
|
10406
|
+
return {
|
|
10407
|
+
error: true,
|
|
10408
|
+
exitCode: 2,
|
|
10409
|
+
text: '拒绝: consolidate --apply 会移动原文并生成摘要(原文保留在 .archive/,可用 --undo 回滚)。请先运行 yotta-memory consolidate 审阅候选报告;交互式执行会要求输入确认串,非交互必须显式加 --yes。',
|
|
10410
|
+
};
|
|
10411
|
+
}
|
|
10412
|
+
const firstRunNotice = consolidateHasManifest(root) ? [] : consolidateFirstRunNotice();
|
|
9160
10413
|
const selfAgent = resolveIdentity(opts).id;
|
|
9161
10414
|
const unsafe = !!opts.unsafe;
|
|
9162
10415
|
const cfg = loadConfig();
|
|
@@ -9168,7 +10421,7 @@ function consolidateCore(opts) {
|
|
|
9168
10421
|
const period = (opts.period !== undefined && opts.period !== null) ? opts.period : (parseInt(cfg.consolidate_period || '90', 10) || 90);
|
|
9169
10422
|
const typeOnly = opts.type ? String(opts.type).toUpperCase() : '';
|
|
9170
10423
|
const lines = ['## yotta-memory consolidate(周期摘要压缩)'];
|
|
9171
|
-
lines.push('- 模式: ' + (apply ? '
|
|
10424
|
+
lines.push('- 模式: ' + (apply ? 'apply(执行压缩)' : 'propose(只出报告,未改动;确认后加 --apply --yes 执行)'));
|
|
9172
10425
|
lines.push('- 候选: 天龄 ≥ ' + minAge + ' 天 ∧ 闲置 ≥ ' + minIdle + ' 天 ∧ utility ≤ ' + maxU + (typeOnly ? ' ∧ 仅类型 ' + typeOnly : '') + ';immutable / BOUND 豁免');
|
|
9173
10426
|
lines.push('');
|
|
9174
10427
|
const cands = [];
|
|
@@ -9191,16 +10444,65 @@ function consolidateCore(opts) {
|
|
|
9191
10444
|
if (age < minAge || idle < minIdle || u > maxU) { stats.active++; continue; }
|
|
9192
10445
|
cands.push({ e: e, owner: owner, type: e.type, age: age, idle: idle, utility: u, scope: e.scope === 'private' ? 'private' : 'public' });
|
|
9193
10446
|
}
|
|
10447
|
+
const makeConsolidateGroup = function (g) {
|
|
10448
|
+
const type = g[0].type;
|
|
10449
|
+
const owner = g[0].scope === 'private' ? (g[0].owner || '') : '';
|
|
10450
|
+
const ages = g.map(function (c) { return c.age; });
|
|
10451
|
+
const theme = pickSummaryTheme(g);
|
|
10452
|
+
return {
|
|
10453
|
+
type: type,
|
|
10454
|
+
scope: g[0].scope === 'private' ? 'private' : 'public',
|
|
10455
|
+
owner: owner,
|
|
10456
|
+
count: g.length,
|
|
10457
|
+
age_min: Math.min.apply(null, ages),
|
|
10458
|
+
age_max: Math.max.apply(null, ages),
|
|
10459
|
+
avg_utility: round3(g.reduce(function (s, c) { return s + c.utility; }, 0) / g.length),
|
|
10460
|
+
theme: theme,
|
|
10461
|
+
summary_subject: '周期摘要 ' + theme + '(' + period + ' 天窗)',
|
|
10462
|
+
files: g.map(function (c) { return c.e.file; }),
|
|
10463
|
+
archive_to: path.relative(root, archiveDirFor(root, type, owner)).replace(/\\/g, '/'),
|
|
10464
|
+
date_normalized_preview: g.reduce(function (s, c) {
|
|
10465
|
+
return s + absolutizeRelativeDates(c.e.statement || '', c.e.created || c.e.updated || '').count;
|
|
10466
|
+
}, 0),
|
|
10467
|
+
};
|
|
10468
|
+
};
|
|
10469
|
+
const makeConsolidateReport = function (builtGroups, batch, extras) {
|
|
10470
|
+
return {
|
|
10471
|
+
schemaVersion: 1,
|
|
10472
|
+
mode: apply ? 'apply' : 'propose',
|
|
10473
|
+
batch: batch || '',
|
|
10474
|
+
root: root,
|
|
10475
|
+
thresholds: {
|
|
10476
|
+
min_age: minAge,
|
|
10477
|
+
min_idle: minIdle,
|
|
10478
|
+
max_utility: maxU,
|
|
10479
|
+
min_group: minGroup,
|
|
10480
|
+
period: period,
|
|
10481
|
+
},
|
|
10482
|
+
stats: stats,
|
|
10483
|
+
candidates: cands.length,
|
|
10484
|
+
groups: builtGroups.map(makeConsolidateGroup),
|
|
10485
|
+
date_normalized: (extras && extras.dateNormalized) || 0,
|
|
10486
|
+
markers_written: (extras && extras.markersWritten) || 0,
|
|
10487
|
+
marker_errors: (extras && extras.markerErrors) || 0,
|
|
10488
|
+
retention: '原文移入 .archive/ 后一直保留,直到显式 purge 或 undo。',
|
|
10489
|
+
rollback: batch
|
|
10490
|
+
? ('yotta-memory consolidate --undo ' + batch)
|
|
10491
|
+
: 'yotta-memory consolidate --undo <batch>(执行后批次号见报告)',
|
|
10492
|
+
};
|
|
10493
|
+
};
|
|
9194
10494
|
if (!cands.length) {
|
|
9195
10495
|
lines.push('(无可压缩候选;扫描 ' + stats.scan + ' 条,跳过: 其它 owner/无密钥 ' + stats.denied + ' / immutable ' + stats.immutable + ' / BOUND ' + stats.bound + ' / 类型过滤 ' + stats.typeSkip + ' / 无 created ' + stats.noCreated + ' / 不够老或仍活跃 ' + stats.active + ')');
|
|
9196
|
-
|
|
10496
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
10497
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport([], '') };
|
|
9197
10498
|
}
|
|
9198
10499
|
const groups = clusterConsolidateCandidates(cands);
|
|
9199
10500
|
lines.push('候选 ' + cands.length + ' 条(扫描 ' + stats.scan + ',跳过: 其它 owner/无密钥 ' + stats.denied + ' / immutable ' + stats.immutable + ' / BOUND ' + stats.bound + ' / 类型过滤 ' + stats.typeSkip + ' / 无 created ' + stats.noCreated + ' / 不够老或仍活跃 ' + stats.active + ')');
|
|
9200
10501
|
if (!groups.length) {
|
|
9201
10502
|
lines.push('');
|
|
9202
10503
|
lines.push('(无可归纳组:同主题聚类 ≥ ' + minGroup + ' 条才生成周期摘要;零散单条旧记忆请用 maintain --apply 归档)');
|
|
9203
|
-
|
|
10504
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
10505
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport([], '') };
|
|
9204
10506
|
}
|
|
9205
10507
|
lines.push('');
|
|
9206
10508
|
lines.push('### 周期摘要组(' + groups.length + ' 组,每组将生成摘要 1 条 + 归档原文 N 条)');
|
|
@@ -9211,12 +10513,19 @@ function consolidateCore(opts) {
|
|
|
9211
10513
|
const minA = Math.min.apply(null, ages), maxA = Math.max.apply(null, ages);
|
|
9212
10514
|
const avgU = round3(g.reduce(function (s, c) { return s + c.utility; }, 0) / g.length);
|
|
9213
10515
|
lines.push('- [' + type + '] 主题: ' + pickSummaryTheme(g) + '(' + scopeLabel + ',' + g.length + ' 条,' + minA + '~' + maxA + ' 天,平均效用 ' + avgU + ')' + (type === 'COMMIT' ? ' ⚠️ COMMIT 承诺/任务类,请确认' : ''));
|
|
10516
|
+
const datePreview = g.reduce(function (s, c) {
|
|
10517
|
+
return s + absolutizeRelativeDates(c.e.statement || '', c.e.created || c.e.updated || '').count;
|
|
10518
|
+
}, 0);
|
|
10519
|
+
lines.push(' → 将生成 周期摘要 ' + pickSummaryTheme(g) + '(' + period + ' 天窗),日期归一预览 ' + datePreview + ' 处,原文归档到 ' + path.relative(root, archiveDirFor(root, type, g[0].owner || '')).replace(/\\/g, '/'));
|
|
9214
10520
|
for (const c of g) lines.push(' · ' + c.e.file + ' — ' + String(c.e.subject || '').slice(0, 32) + ': ' + String(c.e.statement || '').slice(0, 60));
|
|
9215
10521
|
}
|
|
9216
10522
|
if (!apply) {
|
|
9217
10523
|
lines.push('');
|
|
9218
|
-
lines.push('
|
|
9219
|
-
|
|
10524
|
+
lines.push('- 保留期: 原文移入 .archive/ 后一直保留,直到显式 purge 或 undo。');
|
|
10525
|
+
lines.push('- 回滚: 执行后可用 yotta-memory consolidate --undo <batch> 恢复。');
|
|
10526
|
+
lines.push('未执行任何变更。确认后加 --apply(非交互再加 --yes)执行。');
|
|
10527
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
10528
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport(groups, '') };
|
|
9220
10529
|
}
|
|
9221
10530
|
const guard = destructiveGuardCore({
|
|
9222
10531
|
root: root,
|
|
@@ -9233,20 +10542,45 @@ function consolidateCore(opts) {
|
|
|
9233
10542
|
lines.push('');
|
|
9234
10543
|
lines.push('- 事务快照: ' + guard.snapshot.id);
|
|
9235
10544
|
lines.push('- 批次: ' + batch + '(审计 .archive/audit-<日期>.jsonl;回滚: yotta-memory consolidate --undo ' + batch + ')');
|
|
9236
|
-
let made = 0, moved = 0;
|
|
10545
|
+
let made = 0, moved = 0, dateNormalized = 0, markersWritten = 0, markerErrors = 0;
|
|
9237
10546
|
for (const g of groups) {
|
|
9238
|
-
const res = applyConsolidateGroup(root, g, batch, period);
|
|
10547
|
+
const res = applyConsolidateGroup(root, g, batch, period, opts);
|
|
9239
10548
|
made += res.made; moved += res.moved;
|
|
10549
|
+
dateNormalized += res.dateNormalized; markersWritten += res.markersWritten; markerErrors += res.markerErrors;
|
|
9240
10550
|
lines.push('- 已生成摘要 ' + res.summaryRel + '(归档原文 ' + res.moved + ' 条)');
|
|
9241
10551
|
}
|
|
9242
10552
|
appendAudit(root, 'audit', { action: 'batch_done', ts: new Date().toISOString(), batch: batch, command: 'consolidate', summaries: made, archived: moved, active_after: collectEntryFiles(root).length });
|
|
9243
10553
|
lines.push('- 完成: 生成摘要 ' + made + ' 条 / 原文归档 ' + moved + ' 条');
|
|
9244
|
-
|
|
9245
|
-
}
|
|
9246
|
-
|
|
9247
|
-
|
|
10554
|
+
lines.push('- 日期归一: ' + dateNormalized + ' 处;巩固标记: ' + markersWritten + ' 条(失败 ' + markerErrors + ')');
|
|
10555
|
+
if (firstRunNotice.length) { lines.push(''); lines.push.apply(lines, firstRunNotice); }
|
|
10556
|
+
return { error: false, text: lines.join('\n'), report: makeConsolidateReport(groups, batch, { dateNormalized: dateNormalized, markersWritten: markersWritten, markerErrors: markerErrors }) };
|
|
10557
|
+
}
|
|
10558
|
+
|
|
10559
|
+
async function cmdConsolidate(opts) {
|
|
10560
|
+
// v0.18.0 A5:交互式确认串(X 组 / Y 条);非交互明确拒绝并提示 --yes。
|
|
10561
|
+
if (opts.apply && !opts.yes) {
|
|
10562
|
+
if (!process.stdin.isTTY) {
|
|
10563
|
+
const blocked = consolidateCore(opts);
|
|
10564
|
+
console.log(blocked.text);
|
|
10565
|
+
process.exit(blocked.exitCode || 2);
|
|
10566
|
+
}
|
|
10567
|
+
const plan = consolidateCore(Object.assign({}, opts, { apply: false }));
|
|
10568
|
+
const groups = (plan.report && plan.report.groups) || [];
|
|
10569
|
+
const items = groups.reduce(function (s, g) { return s + g.count; }, 0);
|
|
10570
|
+
if (groups.length) {
|
|
10571
|
+
const token = groups.length + ' 组 / ' + items + ' 条';
|
|
10572
|
+
const typed = await promptLine('即将把 ' + token + ' 压缩为摘要(原文保留在 .archive/,可 --undo 回滚)。输入「' + token + '」确认: ');
|
|
10573
|
+
if (typed === null || String(typed).trim() !== token) {
|
|
10574
|
+
console.log('已取消,未做任何改动。');
|
|
10575
|
+
process.exit(2);
|
|
10576
|
+
}
|
|
10577
|
+
}
|
|
10578
|
+
opts = Object.assign({}, opts, { yes: true });
|
|
10579
|
+
}
|
|
9248
10580
|
const r = consolidateCore(opts);
|
|
9249
|
-
console.log(r.
|
|
10581
|
+
if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
|
|
10582
|
+
else console.log(r.text);
|
|
10583
|
+
if (r.exitCode) process.exit(r.exitCode);
|
|
9250
10584
|
if (r.error) process.exit(2);
|
|
9251
10585
|
}
|
|
9252
10586
|
|
|
@@ -9539,6 +10873,11 @@ async function main() {
|
|
|
9539
10873
|
else if (a === '--yes') opts.yes = true;
|
|
9540
10874
|
else if (a === '--keep-memories') opts.keepMemories = true;
|
|
9541
10875
|
else if (a === '--keep-identity') opts.keepIdentity = true;
|
|
10876
|
+
else if (a === '--no-usage') opts.noUsage = true;
|
|
10877
|
+
else if (a === '--capacity') opts.capacity = true;
|
|
10878
|
+
else if (a === '--rules') opts.rules = true;
|
|
10879
|
+
else if (a === '--propose') opts.propose = true;
|
|
10880
|
+
else if (a === '--audit') opts.audit = true;
|
|
9542
10881
|
else if (valueOpts.has(a)) {
|
|
9543
10882
|
const v = args[++i];
|
|
9544
10883
|
if (a === '--type') opts.type = v;
|
|
@@ -9593,6 +10932,7 @@ async function main() {
|
|
|
9593
10932
|
else if (a === '--against') opts.against = v;
|
|
9594
10933
|
else if (a === '--template') opts.template = v;
|
|
9595
10934
|
else if (a === '--path') opts.path = v;
|
|
10935
|
+
else if (a === '--from') opts.from = v;
|
|
9596
10936
|
} else if (a.startsWith('--')) {
|
|
9597
10937
|
if (a === '--query') {
|
|
9598
10938
|
console.error('未知选项: --query。recall/search 的关键词是位置参数:yotta-memory recall [关键词]');
|
|
@@ -9649,7 +10989,7 @@ async function main() {
|
|
|
9649
10989
|
case 'scan': await cmdScan(opts); break;
|
|
9650
10990
|
case 'maintain': cmdMaintain(opts); break;
|
|
9651
10991
|
case 'distill': cmdDistill(opts); break;
|
|
9652
|
-
case 'consolidate': if (opts.undo === true && rest.length) opts.undo = rest[0]; cmdConsolidate(opts); break;
|
|
10992
|
+
case 'consolidate': if (opts.undo === true && rest.length) opts.undo = rest[0]; await cmdConsolidate(opts); break;
|
|
9653
10993
|
case 'forget': cmdForget(rest[0], opts); break;
|
|
9654
10994
|
case 'rename': cmdRename(rest[0], rest[1], opts); break;
|
|
9655
10995
|
case 'archive': cmdArchive(opts); break;
|
|
@@ -9737,10 +11077,19 @@ module.exports = {
|
|
|
9737
11077
|
recallCore: recallCore,
|
|
9738
11078
|
profileCore: profileCore,
|
|
9739
11079
|
contextCore: contextCore,
|
|
11080
|
+
contextAuditCore: contextAuditCore,
|
|
9740
11081
|
importanceScore: importanceScore,
|
|
11082
|
+
usageSettings: usageSettings,
|
|
11083
|
+
usageTotals: usageTotals,
|
|
11084
|
+
usagePatchForMeta: usagePatchForMeta,
|
|
11085
|
+
applyUsageHits: applyUsageHits,
|
|
11086
|
+
parseHitDays: parseHitDays,
|
|
11087
|
+
getOwnerKeyFor: getOwnerKeyFor,
|
|
11088
|
+
loadOwnerIndex: loadOwnerIndex,
|
|
9741
11089
|
forgetCore: forgetCore,
|
|
9742
11090
|
renameCore: renameCore,
|
|
9743
11091
|
archiveCore: archiveCore,
|
|
11092
|
+
capacityCore: capacityCore,
|
|
9744
11093
|
maintainCore: maintainCore,
|
|
9745
11094
|
consolidateCore: consolidateCore,
|
|
9746
11095
|
mergeCore: mergeCore,
|
|
@@ -9760,6 +11109,8 @@ module.exports = {
|
|
|
9760
11109
|
migrateLayout: migrateLayout,
|
|
9761
11110
|
loadIndexFor: loadIndexFor,
|
|
9762
11111
|
indexEntriesFor: indexEntriesFor,
|
|
11112
|
+
loadOwnerIndex: loadOwnerIndex,
|
|
11113
|
+
getOwnerKeyFor: getOwnerKeyFor,
|
|
9763
11114
|
normalizeYearList: normalizeYearList,
|
|
9764
11115
|
indexFingerprint: indexFingerprint,
|
|
9765
11116
|
benchCore: benchCore,
|
|
@@ -9782,6 +11133,10 @@ module.exports = {
|
|
|
9782
11133
|
SCALE_CONFIG_KEYS: SCALE_CONFIG_KEYS,
|
|
9783
11134
|
SCALE_THRESHOLD_DEFAULTS: SCALE_THRESHOLD_DEFAULTS,
|
|
9784
11135
|
scaleReport: scaleReport,
|
|
11136
|
+
USAGE_CONFIG_DEFAULTS: USAGE_CONFIG_DEFAULTS,
|
|
11137
|
+
usageSettings: usageSettings,
|
|
11138
|
+
usageTotals: usageTotals,
|
|
11139
|
+
applyUsageHits: applyUsageHits,
|
|
9785
11140
|
runtimeRoot: runtimeRoot,
|
|
9786
11141
|
runtimeManifestPath: runtimeManifestPath,
|
|
9787
11142
|
runtimeVersionsDir: runtimeVersionsDir,
|