@yottameta/yotta-memory 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## v0.19.0 (2026-09-28)
2
+ - 新增扩展提供方(provider)装载点:`context` 可选调用本地 provider(capability `memory.hook`),在权限过滤后的候选集内驱逐条目;BOUND / COMMIT 与身份画像不可驱逐,预算 / 去重 / 宽限仍由引擎掌控。
3
+ - 未配置 / 未授权 / 超时 / 非法输出一律 fail-open 回普通记忆;`provider.json` 非法只记状态、不阻断。
4
+ - `context --json` 输出结构化 JSON(`schema` / `hook` / `text`);此前该参数回落为文本。
5
+ - 扩展调用只写本地审计元数据(`provider-audit.jsonl`:capability / 状态 / 耗时 / 字节数),不含 payload 正文。
6
+
1
7
  ## v0.18.1 (2026-09-27)
2
8
 
3
9
  第二波 P2:蒸馏溯源链 + 分类型提取 / 相对日期绝对化 + 巩固标记 / 重复踩坑 → 规则晋升建议 / 权威顺序与写入纪律(文档)。
package/README.zh-CN.md CHANGED
@@ -22,6 +22,7 @@
22
22
  </p>
23
23
 
24
24
  > 📖 面向用户的操作手册见 [USER_GUIDE.md](USER_GUIDE.md)。
25
+ > 🆕 **v0.19.0(扩展提供方装载点 + context JSON)**:`context` 可选调用用户显式配置的本地扩展提供方(capability `memory.hook`)参与「哪些记忆进上下文」——提供方只能驱逐本次候选集内条目,BOUND / COMMIT 与身份画像不可驱逐,预算 / 去重 / 宽限仍由引擎掌控;未配置 / 未授权 / 超时 / 非法输出一律回普通记忆。`context --json` 输出结构化 JSON(`schema` / `hook` / `text`)。配置与协议见 `references/provider-protocol.md`。
25
26
  > 🆕 **v0.18.1(蒸馏溯源 + 日期绝对化 + 巩固标记 + 规则晋升建议 + 写入纪律)**:`distill` 新增分类型提取清单(事件 / 教训 / 待办 / 成长 / 规则边界 / 其他)+ 逐条 `[溯源: 文件#L<a>-L<b>]` + 实测质量指标 + 跳过与边界,`--json` 输出结构化报告;`consolidate` 摘要按条目 `created` 把「昨天 / 上周 / 本月 / 今年」写成绝对日期,归档副本写入巩固标记(`--undo` 剥离还原);`maintain --rules` 只读给出「重复踩坑 → 建议升级为规则」(默认阈值 3,不自动写 BOUND);记忆守则新增冲突权威顺序 6 层与写入纪律 3 条。
26
27
 
27
28
  > 🆕 **v0.18.0(命中打点 + 容量水位 + consolidate 提案闸门 + 上下文压缩审计)**:`recall` / `explain` / `feedback --useful` / `context` 记录本地命中元数据(`hit_days` 默认保留 90 天;`hit_queries` 只存查询 8 位指纹、最多 12 槽,不存原文;随文件加密 / 备份 / 导出),`config set usage_enabled false` 或命令级 `--no-usage` 可关;只读命令与跨 owner 私密条目不写。`maintain --capacity` 只读报告容量水位、30 / 90 天活跃度、LRU / LFU 淘汰候选(冷却期 30 天 + immutable / BOUND / evergreen / pinned 豁免)与晋升建议(≥3 次命中且 ≥3 个不同查询,只出建议命令);`archive` 默认豁免冷却期与标签常青,`--force` 可显式覆盖。`consolidate` 默认等价 `--propose`(结构化报告 + `--json`),`--apply` 交互式需确认串、非交互必须 `--yes`;首次启用显示一次数据生命周期说明。`doctor` 规模体检新增 `scale_info_*` 与逐项 `ok / info / warning` 分级(`doctor.ok` 仍只看 critical)。新增 `context --audit [--from <文件|->] [--json] [--gate N]`:核对被压缩掉的内容是否已落盘,输出未落盘清单与 `remember` 建议命令,只读、不自动补写。`archive --dry-run` 只预览不改库(不动文件 / 不建事务快照 / 不写审计),无候选时不建整库快照,`archive --json` 输出结构化报告;MCP 侧只补只读 / 预演能力(`archive.dryRun`、`maintain.capacity`、`context.audit` + 内联 `auditText`、`consolidate` 只出 propose 报告),`archive --force` 与 `consolidate --apply / --undo` 仍只在命令行。上一版 v0.17.4:`rename` 消除平铺 / 分层同序号冲突(跨布局 fail-closed + 破坏性闸门 + 审计)。
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-memory
3
3
  description: 元忆 —— 有权限边界的文件式智能体记忆。文件式、零依赖、可 diff/可回滚:让任何 AI 智能体活过会话,开工 recall 恢复上下文、重要信息 remember 落盘、收工归档。类型体系 FACT(公共共享)/ PREF / BOUND / COMMIT(私密隔离)。触发:记住、别忘了、记一笔、记忆、remember、recall、跨会话、上次说到、续测、交接、归档、记忆盘、共享记忆、局域网记忆、画像、开工上下文、长期理解摘要、近期走廊、会话闭环、记忆守则、profile、context、越用越懂、语义检索、反馈、维护、蒸馏、feedback、maintain、distill、explain、自我学习、自我进化、自我提升、查看平台分页、recall 候选预过滤、任务相关记忆、--focus、--embedding、压缩遗忘、consolidate、周期摘要、自动合并、分类型衰减、回滚、备份、backup、防误删、doctor、事务快照、基线探针、恢复演练、记忆库安全扫描、scan、命中打点、容量水位、--capacity、--propose、上下文压缩审计、context --audit
4
- version: 0.18.1
4
+ version: 0.19.0
5
5
  license: MIT
6
6
  ---
7
7
 
@@ -36,6 +36,7 @@ license: MIT
36
36
  - **可靠性基线(v0.12.0)**:`init` 对非空记忆库默认拒绝覆盖(`--attach` 接入现有库);`forget` 先移入 `.trash/` 并写审计;新增 `backup volumes / setup / status / ensure-daily / schedule / drill`(用户确认真实独立卷后默认每日自动备份)与 `backup create / list / doctor / restore`。
37
37
  - **可靠性收口(v0.12.2)**:新增 `yotta-memory doctor` 开工检查(根目录 / 密钥库 / 索引 / 身份 / 最近备份);`maintain --apply`、`consolidate --apply`、`merge`、`archive`、`--purge` 在写入前自动创建事务快照,快照失败或严重检查异常时拒绝写入。
38
38
  - **运行时 hook 声明(v0.13.0)**:manifest 声明 `after_milestone` / `remember_commit`;里程碑记忆必须有真实文件路径证据才标 verified,缺证据时输出 `explicit-unverified` + 一次纠偏。
39
+ - **扩展提供方装载点(v0.19.0)**:`context` 支持可选的本地扩展提供方(capability `memory.hook`)参与「哪些记忆进上下文」:提供方只能驱逐本次候选集内的条目;身份画像、BOUND / COMMIT、预算 / 去重 / 宽限仍由引擎掌控。未配置 / 未授权 / 超时 / 非法输出一律走普通记忆,输出不带扩展行;`context --json` 输出 `schema` / `hook` / `text`。配置与协议见 `references/provider-protocol.md`。
39
40
 
40
41
  - **蒸馏溯源与分类型提取(v0.18.1)**:`distill` 输出新增「分类型提取清单(事件 / 教训 / 待办 / 成长 / 规则边界 / 其他)+ 质量指标 + 跳过与边界」;每条提取项带 `[溯源: 文件#L<a>-L<b>]`,报告只给实测压缩比 / 条目覆盖 / 溯源覆盖 / 要素提取率,不承诺压缩倍数或语义保真。`distill --json` 输出结构化报告;帮助补 `--owner` / `--out`。
41
42
  - **日期绝对化与巩固标记(v0.18.1)**:`consolidate` 生成摘要正文时,按条目 `created`(缺则 `updated`)把「昨天 / 上周 / 本月 / 今年」等写成绝对日期(如 `昨天(2026-01-09)`);「最近 / 前几天 / 刚才」等模糊词不归一。归档副本末尾追加一行 `<!-- yotta-memory: consolidated to … -->` 巩固标记;`--undo` 剥离标记并还原原始内容;标记失败不影响批次,只在报告与审计里计数。
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ // 扩展提供方(provider)装载器 v1:外部子进程 + stdin/stdout JSON。
4
+ // 设计:docs/路线B-P1-接口预留设计-2026-09-28.md(根仓库);对外协议:references/provider-protocol.md。
5
+ // 零依赖;未配置 / 任何失败都 fail-open 回开源基线,不阻断调用方。
6
+
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+ const { spawnSync } = require('child_process');
12
+
13
+ const SCHEMA = 1;
14
+ const DEFAULT_TIMEOUT_MS = 600;
15
+ const MAX_TIMEOUT_MS = 5000;
16
+ const MIN_TIMEOUT_MS = 50;
17
+ const MAX_STDOUT_BYTES = 256 * 1024;
18
+ const KNOWN_CAPABILITIES = ['o1.route', 'm1.adjudicate', 'memory.hook', 'context.paging'];
19
+
20
+ function providerHome() {
21
+ const fromEnv = process.env.YOTTA_PROVIDER_HOME;
22
+ if (fromEnv && String(fromEnv).trim()) return path.resolve(String(fromEnv).trim());
23
+ return path.join(os.homedir(), '.yottameta');
24
+ }
25
+
26
+ function configPath() {
27
+ return path.join(providerHome(), 'provider.json');
28
+ }
29
+
30
+ function validateProvider(item) {
31
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return 'provider 条目必须是对象';
32
+ if (!item.id || typeof item.id !== 'string') return 'provider 缺 id';
33
+ if (!Array.isArray(item.capabilities) || !item.capabilities.length) return 'provider ' + item.id + ' 缺 capabilities';
34
+ for (const capability of item.capabilities) {
35
+ if (KNOWN_CAPABILITIES.indexOf(capability) === -1) return 'provider ' + item.id + ' 含未知 capability:' + capability;
36
+ }
37
+ if (!Array.isArray(item.command) || !item.command.length) return 'provider ' + item.id + ' 的 command 必须是数组';
38
+ for (const part of item.command) {
39
+ if (typeof part !== 'string' || !part) return 'provider ' + item.id + ' 的 command 含非法参数';
40
+ if (part.indexOf('\0') !== -1) return 'provider ' + item.id + ' 的 command 含非法字符';
41
+ }
42
+ if (item.timeout_ms !== undefined && (!Number.isFinite(item.timeout_ms) || item.timeout_ms <= 0)) {
43
+ return 'provider ' + item.id + ' 的 timeout_ms 非法';
44
+ }
45
+ return '';
46
+ }
47
+
48
+ function normalizeProvider(item) {
49
+ return {
50
+ id: item.id,
51
+ version: typeof item.version === 'string' ? item.version : '',
52
+ capabilities: item.capabilities.slice(),
53
+ command: item.command.slice(),
54
+ timeout_ms: Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.round(item.timeout_ms || DEFAULT_TIMEOUT_MS))),
55
+ };
56
+ }
57
+
58
+ function readConfig() {
59
+ const file = configPath();
60
+ if (!fs.existsSync(file)) return { status: 'not_installed', providers: [], error: '' };
61
+ let raw;
62
+ try {
63
+ raw = JSON.parse(fs.readFileSync(file, 'utf8'));
64
+ } catch (e) {
65
+ return { status: 'error', providers: [], error: 'provider.json 解析失败:' + e.message };
66
+ }
67
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
68
+ return { status: 'error', providers: [], error: 'provider.json 必须是对象' };
69
+ }
70
+ if (raw.schema !== SCHEMA) {
71
+ return { status: 'error', providers: [], error: 'provider.json schema 不支持:' + String(raw.schema) };
72
+ }
73
+ if (!Array.isArray(raw.providers)) {
74
+ return { status: 'error', providers: [], error: 'provider.json providers 必须是数组' };
75
+ }
76
+ const providers = [];
77
+ for (const item of raw.providers) {
78
+ const invalid = validateProvider(item);
79
+ if (invalid) return { status: 'error', providers: [], error: invalid };
80
+ providers.push(normalizeProvider(item));
81
+ }
82
+ return { status: 'ok', providers, error: '' };
83
+ }
84
+
85
+ function pickProvider(config, capability) {
86
+ if (!config || config.status !== 'ok') return null;
87
+ for (const item of config.providers) {
88
+ if (item.capabilities.indexOf(capability) !== -1) return item;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function minimalEnv() {
94
+ const keep = [
95
+ 'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'COMSPEC', 'ComSpec',
96
+ 'TEMP', 'TMP', 'HOME', 'USERPROFILE', 'LANG', 'LC_ALL',
97
+ ];
98
+ const env = {};
99
+ for (const key of keep) {
100
+ if (process.env[key] !== undefined) env[key] = process.env[key];
101
+ }
102
+ env.YOTTA_PROVIDER_HOME = providerHome();
103
+ return env;
104
+ }
105
+
106
+ function requestId() {
107
+ try {
108
+ if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
109
+ } catch (e) {
110
+ // 回退到随机 hex
111
+ }
112
+ return crypto.randomBytes(16).toString('hex');
113
+ }
114
+
115
+ function invokeProvider(provider, capability, payload) {
116
+ const started = Date.now();
117
+ const request = {
118
+ schema: SCHEMA,
119
+ capability,
120
+ request_id: requestId(),
121
+ payload: payload || {},
122
+ };
123
+ let proc;
124
+ try {
125
+ proc = spawnSync(provider.command[0], provider.command.slice(1), {
126
+ input: JSON.stringify(request),
127
+ encoding: 'utf8',
128
+ timeout: provider.timeout_ms,
129
+ maxBuffer: MAX_STDOUT_BYTES,
130
+ shell: false,
131
+ cwd: providerHome(),
132
+ env: minimalEnv(),
133
+ windowsHide: true,
134
+ });
135
+ } catch (e) {
136
+ return {
137
+ status: 'error',
138
+ provider_id: provider.id,
139
+ duration_ms: Date.now() - started,
140
+ note: 'provider 启动失败:' + e.message,
141
+ };
142
+ }
143
+ const duration = Date.now() - started;
144
+ if (proc.error) {
145
+ const code = proc.error.code || '';
146
+ const timedOut = code === 'ETIMEDOUT';
147
+ const oversize = code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
148
+ return {
149
+ status: timedOut ? 'timeout' : 'error',
150
+ provider_id: provider.id,
151
+ duration_ms: duration,
152
+ note: timedOut
153
+ ? 'provider 超时'
154
+ : (oversize
155
+ ? 'provider 输出超过 ' + MAX_STDOUT_BYTES + ' 字节'
156
+ : 'provider 执行失败:' + (code || proc.error.message || 'unknown')),
157
+ };
158
+ }
159
+ if (proc.status !== 0) {
160
+ return {
161
+ status: 'error',
162
+ provider_id: provider.id,
163
+ duration_ms: duration,
164
+ note: 'provider 退出码非 0:' + proc.status,
165
+ };
166
+ }
167
+ let response;
168
+ try {
169
+ response = JSON.parse(String(proc.stdout || '').trim() || '{}');
170
+ } catch (e) {
171
+ return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是 JSON' };
172
+ }
173
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
174
+ return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是对象' };
175
+ }
176
+ if (response.ok === false) {
177
+ const code = typeof response.code === 'string' && response.code ? response.code : 'provider_error';
178
+ return {
179
+ status: code === 'license_required' ? 'license_required' : 'error',
180
+ provider_id: provider.id,
181
+ code,
182
+ message: typeof response.message === 'string' ? response.message : '',
183
+ duration_ms: duration,
184
+ note: typeof response.message === 'string' ? response.message : code,
185
+ };
186
+ }
187
+ if (response.ok !== true) {
188
+ return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 响应缺 ok 字段' };
189
+ }
190
+ return {
191
+ status: 'active',
192
+ provider_id: provider.id,
193
+ data: response.data,
194
+ duration_ms: duration,
195
+ bytes_out: Buffer.byteLength(String(proc.stdout || ''), 'utf8'),
196
+ note: '',
197
+ };
198
+ }
199
+
200
+ function appendAudit(entry) {
201
+ try {
202
+ const home = providerHome();
203
+ fs.mkdirSync(home, { recursive: true });
204
+ fs.appendFileSync(
205
+ path.join(home, 'provider-audit.jsonl'),
206
+ JSON.stringify(Object.assign({ schema: SCHEMA, ts: new Date().toISOString() }, entry)) + '\n',
207
+ 'utf8',
208
+ );
209
+ } catch (e) {
210
+ // 审计失败不影响调用
211
+ }
212
+ }
213
+
214
+ function runCapability(capability, payload) {
215
+ const config = readConfig();
216
+ if (config.status === 'not_installed') {
217
+ return { status: 'not_installed', provider_id: '', note: '未配置扩展提供方' };
218
+ }
219
+ if (config.status === 'error') {
220
+ return { status: 'error', provider_id: '', note: config.error };
221
+ }
222
+ const provider = pickProvider(config, capability);
223
+ if (!provider) {
224
+ return { status: 'not_installed', provider_id: '', note: '未配置声明 ' + capability + ' 的提供方' };
225
+ }
226
+ const result = invokeProvider(provider, capability, payload);
227
+ appendAudit({
228
+ capability,
229
+ provider_id: result.provider_id,
230
+ status: result.status,
231
+ duration_ms: result.duration_ms,
232
+ bytes_out: result.bytes_out || 0,
233
+ });
234
+ return result;
235
+ }
236
+
237
+ module.exports = {
238
+ SCHEMA,
239
+ DEFAULT_TIMEOUT_MS,
240
+ MAX_TIMEOUT_MS,
241
+ MAX_STDOUT_BYTES,
242
+ KNOWN_CAPABILITIES,
243
+ providerHome,
244
+ configPath,
245
+ readConfig,
246
+ pickProvider,
247
+ runCapability,
248
+ };
@@ -25,7 +25,7 @@ const net = require('net');
25
25
  const child_process = require('child_process');
26
26
  const { AsyncLocalStorage } = require('async_hooks');
27
27
 
28
- const VERSION = '0.18.1';
28
+ const VERSION = '0.19.0';
29
29
  const CLI_VALUE_OPTS = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--plugin-data', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--recovery-key-out', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir', '--year', '--evalset', '--k', '--seed', '--bootstrap', '--gate', '--against', '--template', '--path', '--from']);
30
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
 
@@ -210,6 +210,7 @@ const HELP_MODEL = [
210
210
  helpOption('--audit', '', '审计被压缩掉的内容是否已经落盘', '宿主压缩上下文后,想确认有没有决策只存在于对话里时', '只读:只打印 remember 建议,不会自动补写'),
211
211
  helpOption('--from', '<文件|->', '指定待审计内容(- 表示标准输入)', '把宿主压缩摘要或丢弃段落喂给审计时', '不传时审计当前上下文包的丢弃清单'),
212
212
  helpOption('--limit', '<条数>', '限制近期记忆条数', '上下文太长、想缩短时', ''),
213
+ helpOption('--json', '', '输出结构化 JSON(hook 块 + text)', '脚本读取上下文包或扩展装载状态时', '未配置扩展提供方时也返回 JSON'),
213
214
  helpOption('--owner', '<id>', '指定 owner 范围', '需要看某个 owner 的上下文时', '仍然受私密区权限约束'),
214
215
  helpOption('--budget', '<字符数>', '设置动态记忆字符预算', '需要控制上下文包大小时', '0 表示不限制'),
215
216
  helpOption('--focus', '<关键词>', '按当前任务聚焦', '这次任务很明确、想优先拉相关记忆时', ''),
@@ -7561,6 +7562,109 @@ function compareContextRecent(a, b) {
7561
7562
  if (byTime !== 0) return byTime;
7562
7563
  return (b.access_count || 0) - (a.access_count || 0);
7563
7564
  }
7565
+
7566
+ const MEMORY_HOOK_MAX_CANDIDATES = 500;
7567
+ const MEMORY_HOOK_STATEMENT_LIMIT = 2000;
7568
+
7569
+ function hookBlock(status, providerId, note) {
7570
+ return {
7571
+ status: status || 'not_installed',
7572
+ provider_id: providerId || '',
7573
+ applied: false,
7574
+ evicted: [],
7575
+ dropped: [],
7576
+ note: note || '',
7577
+ };
7578
+ }
7579
+
7580
+ function hookStatusText(hook) {
7581
+ if (!hook) return '';
7582
+ if (hook.status === 'active') {
7583
+ const who = hook.provider_id ? '提供方 ' + hook.provider_id : '提供方';
7584
+ return '已应用(' + who + ';驱逐 ' + hook.evicted.length + ' / 丢弃 ' + hook.dropped.length + ')';
7585
+ }
7586
+ if (hook.status === 'license_required') return '需授权(该能力需要授权后使用;普通记忆不受影响)';
7587
+ if (hook.status === 'timeout') return '未生效(提供方超时,已回落普通记忆)';
7588
+ if (hook.status === 'invalid_output') return '未生效(提供方输出无效,已回落普通记忆)';
7589
+ if (hook.status === 'error') return '未生效(提供方异常,已回落普通记忆)';
7590
+ return hook.status;
7591
+ }
7592
+
7593
+ /**
7594
+ * memory.hook 装载点:只把「可驱逐」条目(非 BOUND / COMMIT)交给 provider。
7595
+ * provider 只能返回本次候选集内的 evict(驱逐清单);complete=true 时可选 selected(白名单)。
7596
+ * 身份画像、BOUND / COMMIT、预算 / 去重 / 宽限始终由引擎掌控;任何失败都走普通记忆。
7597
+ * 协议见 references/provider-protocol.md。
7598
+ */
7599
+ function applyMemoryHook(readableEntries, opts) {
7600
+ const block = hookBlock('not_installed', '', '');
7601
+ const pageable = readableEntries
7602
+ .filter(function (entry) { return entry.type !== 'BOUND' && entry.type !== 'COMMIT'; })
7603
+ .sort(compareContextRecent);
7604
+ const truncated = pageable.length > MEMORY_HOOK_MAX_CANDIDATES;
7605
+ const payload = {
7606
+ agent: opts.owner || '',
7607
+ budget: opts.budget || 0,
7608
+ focus: opts.focus || '',
7609
+ truncated: truncated,
7610
+ candidates: pageable.slice(0, MEMORY_HOOK_MAX_CANDIDATES).map(function (entry) {
7611
+ return {
7612
+ file: entry.file,
7613
+ type: entry.type,
7614
+ subject: entry.subject,
7615
+ statement: String(entry.statement || '').slice(0, MEMORY_HOOK_STATEMENT_LIMIT),
7616
+ created: entry.created || '',
7617
+ updated: entry.updated || '',
7618
+ };
7619
+ }),
7620
+ };
7621
+ let run;
7622
+ try {
7623
+ run = require('./provider').runCapability('memory.hook', payload);
7624
+ } catch (e) {
7625
+ block.status = 'error';
7626
+ block.note = '扩展装载失败:' + e.message;
7627
+ return { block: block, evict: new Set(), selected: null };
7628
+ }
7629
+ block.status = run.status;
7630
+ block.provider_id = run.provider_id || '';
7631
+ block.note = run.note || run.message || '';
7632
+ if (run.status !== 'active' || !run.data || typeof run.data !== 'object') {
7633
+ return { block: block, evict: new Set(), selected: null };
7634
+ }
7635
+ const sent = new Set(payload.candidates.map(function (item) { return item.file; }));
7636
+ const evicted = [];
7637
+ for (const item of (Array.isArray(run.data.evict) ? run.data.evict : [])) {
7638
+ const file = String(item || '');
7639
+ if (!file) continue;
7640
+ if (!sent.has(file)) {
7641
+ block.dropped.push(file);
7642
+ continue;
7643
+ }
7644
+ if (evicted.indexOf(file) === -1) evicted.push(file);
7645
+ }
7646
+ let selected = null;
7647
+ if (Array.isArray(run.data.selected) && run.data.complete === true && !truncated) {
7648
+ const picked = [];
7649
+ for (const item of run.data.selected) {
7650
+ const file = String(item || '');
7651
+ if (!file) continue;
7652
+ if (!sent.has(file)) {
7653
+ block.dropped.push(file);
7654
+ continue;
7655
+ }
7656
+ if (picked.indexOf(file) === -1) picked.push(file);
7657
+ }
7658
+ selected = new Set(picked);
7659
+ }
7660
+ block.evicted = evicted;
7661
+ block.applied = evicted.length > 0 || selected !== null;
7662
+ if (truncated && Array.isArray(run.data.selected)) {
7663
+ block.note = block.note || '候选超过 ' + MEMORY_HOOK_MAX_CANDIDATES + ',白名单模式未应用(保护)';
7664
+ }
7665
+ return { block: block, evict: new Set(evicted), selected: selected };
7666
+ }
7667
+
7564
7668
  function contextCore(opts) {
7565
7669
  opts = opts || {};
7566
7670
  const roots = memoryRoots();
@@ -7672,9 +7776,26 @@ function contextCore(opts) {
7672
7776
  }
7673
7777
  }
7674
7778
 
7779
+ const memHook = applyMemoryHook(readableEntries, { owner: owner, budget: budget, focus: focus, limit: limit });
7780
+ const hookState = memHook.block;
7781
+ function hookAllows(entry) {
7782
+ if (!entry) return false;
7783
+ if (memHook.selected && !memHook.selected.has(entry.file)) return false;
7784
+ if (memHook.evict.has(entry.file)) return false;
7785
+ return true;
7786
+ }
7787
+ if (hookState.status !== 'not_installed') {
7788
+ const anchor = lines.indexOf('## 1. 身份');
7789
+ lines.splice(anchor === -1 ? 2 : anchor, 0, '- 扩展装载(memory.hook): ' + hookStatusText(hookState), '');
7790
+ trace.push('[hook] status: ' + hookState.status
7791
+ + ' provider: ' + (hookState.provider_id || '-')
7792
+ + ' evicted: ' + hookState.evicted.length
7793
+ + ' dropped: ' + hookState.dropped.length);
7794
+ }
7795
+
7675
7796
  lines.push('## 2.5 长期理解摘要');
7676
7797
  lines.push('');
7677
- const summaries = readableEntries.filter(isConsolidatedSummary).sort(compareContextRecent);
7798
+ const summaries = readableEntries.filter(isConsolidatedSummary).filter(hookAllows).sort(compareContextRecent);
7678
7799
  const summaryLimit = Math.min(3, Math.max(0, limit));
7679
7800
  if (!summaries.length) lines.push('(暂无周期摘要;可用 yotta-memory consolidate --apply 生成)');
7680
7801
  for (const e of summaries.slice(0, summaryLimit)) {
@@ -7697,7 +7818,7 @@ function contextCore(opts) {
7697
7818
  });
7698
7819
  const focusedEntries = focused.entries || [];
7699
7820
  if (!focusedEntries.length) lines.push('(无匹配记忆)');
7700
- for (const e of focusedEntries) {
7821
+ for (const e of focusedEntries.filter(hookAllows)) {
7701
7822
  appendContextEntry(e, 'focus_match score: ' + round3(e.score));
7702
7823
  }
7703
7824
  lines.push('');
@@ -7708,6 +7829,7 @@ function contextCore(opts) {
7708
7829
  const corridor = readableEntries
7709
7830
  .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT'; })
7710
7831
  .sort(compareContextRecent)
7832
+ .filter(hookAllows)
7711
7833
  .filter(function (e) { return !shownFiles.has(e.file); })
7712
7834
  .slice(0, limit);
7713
7835
  if (!corridor.length) lines.push('(暂无近期记忆)');
@@ -7717,7 +7839,7 @@ function contextCore(opts) {
7717
7839
  lines.push('## 4. 近期高价值记忆(补位)');
7718
7840
  lines.push('');
7719
7841
  const highValue = readableEntries
7720
- .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file); })
7842
+ .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file) && hookAllows(e); })
7721
7843
  .map(function (e) { return { e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) }; })
7722
7844
  .sort(function (a, b) { return b.s - a.s; })
7723
7845
  .slice(0, limit);
@@ -7777,12 +7899,16 @@ function contextCore(opts) {
7777
7899
  for (const message of reliability.warnings) lines.push('- [警告] ' + message);
7778
7900
  if (!reliability.ok) lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
7779
7901
  }
7780
- return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace };
7902
+ return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace, hook: hookState };
7781
7903
  }
7782
7904
  function cmdContext(opts) {
7783
7905
  const r = contextCore(opts);
7784
- if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
7785
- else console.log(r.text);
7906
+ if (opts.json) {
7907
+ if (r.report) console.log(JSON.stringify(r.report, null, 2));
7908
+ else console.log(JSON.stringify({ schema: 1, hook: r.hook || null, text: r.text }, null, 2));
7909
+ } else {
7910
+ console.log(r.text);
7911
+ }
7786
7912
  if (r.error) {
7787
7913
  const reminder = migrationReminderText(userRoot());
7788
7914
  if (reminder) console.log('\n' + reminder);
@@ -11078,6 +11204,7 @@ module.exports = {
11078
11204
  profileCore: profileCore,
11079
11205
  contextCore: contextCore,
11080
11206
  contextAuditCore: contextAuditCore,
11207
+ applyMemoryHook: applyMemoryHook,
11081
11208
  importanceScore: importanceScore,
11082
11209
  usageSettings: usageSettings,
11083
11210
  usageTotals: usageTotals,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "Yuanyi (元忆) — boundary-aware, file-based memory for AI agents. File-based, zero-dependency, diff/rollback-able; FACT/PREF/BOUND/COMMIT types (public shared / private isolated), user-level + project-level storage; v0.12 reliability baseline (init guard, trash-based deletion, independent-volume backup/list/doctor/restore, start-of-work doctor, transactional snapshots before destructive writes), v0.10 consolidation (periodic summaries with provenance, near-duplicate auto-merge, per-type decay, batch audit + rollback), v0.9 recall quality + context focus + optional local embedding plugin, plus v0.8 semantic search, feedback loop, self-organization and distillation.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -0,0 +1,100 @@
1
+ # 扩展提供方协议 v1(元忆 · capability `memory.hook`)
2
+
3
+ 元忆可以可选地调用一个由用户显式配置的**本地扩展提供方(provider)**,由它参与「哪些记忆进上下文」。
4
+ 未配置提供方时,`context` 的行为与输出与之前完全一致;任何失败都回落到普通记忆,不阻断命令。
5
+
6
+ ## 1. 配置
7
+
8
+ 配置文件:`<YOTTA_PROVIDER_HOME>/provider.json`,默认 `~/.yottameta/provider.json`。
9
+ 环境变量 `YOTTA_PROVIDER_HOME` 可覆盖根目录(测试与隔离环境用)。
10
+
11
+ ```json
12
+ {
13
+ "schema": 1,
14
+ "providers": [
15
+ {
16
+ "id": "local-provider",
17
+ "version": "0.1.0",
18
+ "capabilities": ["memory.hook"],
19
+ "command": ["node", "C:/path/to/provider.js"],
20
+ "timeout_ms": 600
21
+ }
22
+ ]
23
+ }
24
+ ```
25
+
26
+ - `command` 必须是**数组**(argv 语义),以 `shell: false` 执行;不接受字符串命令。
27
+ - `timeout_ms` 默认 600,最小 50,最大 5000;超时即回落。
28
+ - 配置文件缺失、解析失败、`command` 非数组、capability 未知:**只记录状态,不阻断命令**;缺失等同「未安装」。
29
+
30
+ ## 2. 调用
31
+
32
+ - 只在用户显式执行 `context`(或 `context --json` / `--explain`)时触发;`init` / `install` / `doctor` / `scan` 等路径不触发。
33
+ - 元忆把**一个 JSON 请求**写入 provider 的 stdin(随后关闭),从 stdout 读**一个 JSON 响应**;stderr 只作诊断。
34
+ - stdout 上限 256 KB;超过按错误处理并回落。
35
+ - 请求与环境:
36
+
37
+ ```json
38
+ {
39
+ "schema": 1,
40
+ "capability": "memory.hook",
41
+ "request_id": "<uuid>",
42
+ "payload": {
43
+ "agent": "codex",
44
+ "budget": 0,
45
+ "focus": "",
46
+ "truncated": false,
47
+ "candidates": [
48
+ {
49
+ "file": "facts/2026/09/2026-09-27-0001.md",
50
+ "type": "FACT",
51
+ "subject": "示例主题",
52
+ "statement": "示例内容(最多 2000 字)",
53
+ "created": "2026-09-27",
54
+ "updated": "2026-09-27"
55
+ }
56
+ ]
57
+ }
58
+ }
59
+ ```
60
+
61
+ - `candidates` 只包含**调用者有权读取**、且**可驱逐**的条目(已过滤私密越权项;BOUND / COMMIT 不在其中)。
62
+ - 候选最多 500 条;超过时 `truncated = true`,此时白名单模式不生效(见 §3),驱逐模式仍安全。
63
+
64
+ ## 3. 响应
65
+
66
+ ```json
67
+ { "ok": true, "capability": "memory.hook", "data": { "evict": ["facts/2026/09/2026-09-27-0001.md"] } }
68
+ ```
69
+
70
+ - **`evict`(推荐)**:要从上下文里驱逐的条目清单。每个 `file` 必须出现在本次 `candidates` 中;候选集外的 file 会被丢弃并记入 `dropped`。未见过的条目默认保留 —— 候选很多时也安全。
71
+ - **`selected`(可选白名单)**:仅在 `complete: true` 且本次未 `truncated` 时接受。每个 `file` 必须 ∈ `candidates`;未列入的候选会被驱逐。
72
+ - 元忆侧仍强制执行:BOUND / COMMIT 与身份画像不可驱逐;预算、去重、宽限、章节顺序由引擎决定。
73
+
74
+ 需要授权或不可用时:
75
+
76
+ ```json
77
+ { "ok": false, "code": "license_required", "message": "该能力需要授权后使用" }
78
+ ```
79
+
80
+ ## 4. 状态与回落
81
+
82
+ | 状态 | 触发 | 元忆行为 |
83
+ | --- | --- | --- |
84
+ | `not_installed` | 无配置 / 无匹配 capability | 普通记忆,输出与历史一致 |
85
+ | `active` | 调用成功且响应合法 | 在约束内应用 `evict` / `selected` |
86
+ | `license_required` | provider 明确返回该 code | 普通记忆 + 一行状态提示 |
87
+ | `timeout` | 超过 `timeout_ms` | 普通记忆 + 一行状态提示 |
88
+ | `invalid_output` | 非 JSON / 缺 `ok` | 普通记忆 + 一行状态提示 |
89
+ | `error` | 启动失败 / 退出码非 0 / 输出超限 / 配置非法 | 普通记忆 + 一行状态提示 |
90
+
91
+ `context --json` 的 `hook` 块给出 `status` / `provider_id` / `applied` / `evicted` / `dropped` / `note`;
92
+ `context --explain` 的 trace 里追加一行 `[hook] ...`。
93
+
94
+ ## 5. 审计与边界
95
+
96
+ - 每次实际调用写一行 `<YOTTA_PROVIDER_HOME>/provider-audit.jsonl`:`ts` / `capability` / `provider_id` / `status` / `duration_ms` / `bytes_out`。
97
+ - 审计**不记录**记忆正文、查询原文或任何 payload 内容。
98
+ - 元忆不替 provider 联网;provider 自身行为由它自己的包声明。
99
+ - 删除 `provider.json` 即回到普通记忆,无残留依赖。
100
+ - provider 输出只当数据使用:候选集外的条目、非白名单内容一律丢弃,不作为指令执行。
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-memory",
4
4
  "name": "元忆",
5
5
  "package": "@yottameta/yotta-memory",
6
- "version": "0.18.1",
6
+ "version": "0.19.0",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true
@@ -12,7 +12,7 @@
12
12
  "filesystem": "user-skills-dir",
13
13
  "network": "none",
14
14
  "process": "child-process",
15
- "note": "只调用本包内 Node 引擎读写本地记忆库;默认不联网。"
15
+ "note": "只调用本包内 Node 引擎读写本地记忆库;默认不联网;仅在用户显式配置扩展提供方(provider.json)时按其配置调用本地子进程。"
16
16
  },
17
17
  "auto_apply": {
18
18
  "mode": "hook",