@yottameta/yotta-memory 0.18.1 → 0.20.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,20 @@
1
+ ## v0.20.0 (2026-09-30)
2
+
3
+ 上下文分页 P2.5:先修 `memory.hook` 的 PREF 驱逐空转,再新增 `context.paging` 预算调用点。
4
+
5
+ - 修复 `memory.hook` 对画像段落驱逐空转:hook / paging 决策实际生效时,画像段落按实时可读条目渲染并套用同一套过滤;自我接入档案不进候选、不可驱逐;无 provider 时仍读 `profile.md`,基线输出逐字节不变。
6
+ - 修复 `profile` 只读单层目录:`private/<owner>/{prefs,bounds,commits}/YYYY/MM/` 里的分层条目现在也会进入静态画像;`context` 的实时画像路径不受影响。
7
+ - 新增 `context.paging`:仅在用户显式传 `--budget` 时调用;`memory.hook` 先过滤,`context.paging` 再排序 / 分页。`drop` 始终安全应用;`order` 仅在 `complete:true` 且候选未超过 500 时应用;候选集外文件只记 note、不进入输出。
8
+ - `context --json` 新增 `paging` 块(`status` / `provider_id` / `applied` / `dropped` / `ordered` / `truncated` / `budget` / `used` / `note`);文本仅在 `context.paging` 实际被声明或触发时增加状态行;审计沿用 `provider-audit.jsonl`,不含记忆正文。
9
+ - provider 子进程透传 `YOTTA_LICENSE_HOME` / `YOTTA_LICENSE_KEYS_DIR` / `YOTTA_LICENSE_BASE_URL` / `YOTTA_LICENSE_SERVER_ID`,与路线 B 授权隔离环境一致。
10
+ - 协议文档升级 v1.1,新增 `context.paging` 请求 / 响应 / 回落说明。
11
+
12
+ ## v0.19.0 (2026-09-28)
13
+ - 新增扩展提供方(provider)装载点:`context` 可选调用本地 provider(capability `memory.hook`),在权限过滤后的候选集内驱逐条目;BOUND / COMMIT 与身份画像不可驱逐,预算 / 去重 / 宽限仍由引擎掌控。
14
+ - 未配置 / 未授权 / 超时 / 非法输出一律 fail-open 回普通记忆;`provider.json` 非法只记状态、不阻断。
15
+ - `context --json` 输出结构化 JSON(`schema` / `hook` / `text`);此前该参数回落为文本。
16
+ - 扩展调用只写本地审计元数据(`provider-audit.jsonl`:capability / 状态 / 耗时 / 字节数),不含 payload 正文。
17
+
1
18
  ## v0.18.1 (2026-09-27)
2
19
 
3
20
  第二波 P2:蒸馏溯源链 + 分类型提取 / 相对日期绝对化 + 巩固标记 / 重复踩坑 → 规则晋升建议 / 权威顺序与写入纪律(文档)。
package/README.md CHANGED
@@ -107,7 +107,7 @@ Each agent has a globally unique agent ID: it is the ownership key for private m
107
107
  ### Profile & start-of-work context (v0.6.0 + v0.9.0 + v0.14.0)
108
108
 
109
109
  - **profile**: aggregates `private/<owner>/` PREF / BOUND / COMMIT verbatim, grouped by type + subject + tags, written to `profile.md`; the engine infers nothing — profile conclusions are formed internally by the AI per the "memory discipline", never pasted as labels.
110
- - **context**: one-shot start-of-work package — multi-agent integration rules + identity + long-term summaries first (`consolidate` output) + user profile digest + optional task-focused memory (`--focus`) + time-ordered recent corridor + deduplicated high-value backfill + boundary reminders + commitments/anchors + session loop contract; supports `--budget` for dynamic memory and `--explain` selection trace.
110
+ - **context**: one-shot start-of-work package — multi-agent integration rules + identity + long-term summaries first (`consolidate` output) + user profile digest + optional task-focused memory (`--focus`) + time-ordered recent corridor + deduplicated high-value backfill + boundary reminders + commitments/anchors + session loop contract; supports `--budget` for dynamic memory and `--explain` selection trace. Optional local providers can add `memory.hook` filtering and, when `--budget` is explicit, `context.paging` ordering/paging; `context --json` returns `hook` and `paging` blocks.
111
111
  - **Memory discipline**: SKILL.md embeds a rule layer (type red lines / proactive trigger capture / know-the-user three stages / psychological grounding & alignment / bottom lines / host isolation / anti-patterns).
112
112
 
113
113
  ### Retrieval: semantic search (v0.8.0 + v0.9.0 embedding)
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.20.0(上下文分页 + 画像驱逐修复)**:`memory.hook` 的 PREF 驱逐空转已修复——hook / paging 决策生效时,画像段落按实时可读条目渲染并套用同一套过滤;自我接入档案不进候选、不可驱逐。新增 `context.paging`:仅在显式 `--budget` 时调用,hook 先过滤、paging 再排序与分页;`drop` 始终安全应用,`order` 仅在 `complete:true` 且未截断时应用。`context --json` 新增 `paging` 块,`--explain` 增加 `[paging]` 轨迹。配置与协议见 `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.20.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.20.0)**:`context` 支持可选的本地扩展提供方参与「哪些记忆进上下文」:`memory.hook` 先按候选集驱逐 / 白名单过滤,`context.paging` 在显式 `--budget` 时再排序与分页。身份段落、自我接入档案、BOUND / COMMIT、预算 / 去重 / 宽限仍由引擎掌控;未配置 / 未授权 / 超时 / 非法输出一律走普通记忆。`context --json` 输出 `schema` / `hook` / `paging` / `text`,`--explain` 增加 `[paging]` 轨迹。配置与协议见 `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,249 @@
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
+ 'YOTTA_LICENSE_HOME', 'YOTTA_LICENSE_KEYS_DIR', 'YOTTA_LICENSE_BASE_URL', 'YOTTA_LICENSE_SERVER_ID',
98
+ ];
99
+ const env = {};
100
+ for (const key of keep) {
101
+ if (process.env[key] !== undefined) env[key] = process.env[key];
102
+ }
103
+ env.YOTTA_PROVIDER_HOME = providerHome();
104
+ return env;
105
+ }
106
+
107
+ function requestId() {
108
+ try {
109
+ if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
110
+ } catch (e) {
111
+ // 回退到随机 hex
112
+ }
113
+ return crypto.randomBytes(16).toString('hex');
114
+ }
115
+
116
+ function invokeProvider(provider, capability, payload) {
117
+ const started = Date.now();
118
+ const request = {
119
+ schema: SCHEMA,
120
+ capability,
121
+ request_id: requestId(),
122
+ payload: payload || {},
123
+ };
124
+ let proc;
125
+ try {
126
+ proc = spawnSync(provider.command[0], provider.command.slice(1), {
127
+ input: JSON.stringify(request),
128
+ encoding: 'utf8',
129
+ timeout: provider.timeout_ms,
130
+ maxBuffer: MAX_STDOUT_BYTES,
131
+ shell: false,
132
+ cwd: providerHome(),
133
+ env: minimalEnv(),
134
+ windowsHide: true,
135
+ });
136
+ } catch (e) {
137
+ return {
138
+ status: 'error',
139
+ provider_id: provider.id,
140
+ duration_ms: Date.now() - started,
141
+ note: 'provider 启动失败:' + e.message,
142
+ };
143
+ }
144
+ const duration = Date.now() - started;
145
+ if (proc.error) {
146
+ const code = proc.error.code || '';
147
+ const timedOut = code === 'ETIMEDOUT';
148
+ const oversize = code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
149
+ return {
150
+ status: timedOut ? 'timeout' : 'error',
151
+ provider_id: provider.id,
152
+ duration_ms: duration,
153
+ note: timedOut
154
+ ? 'provider 超时'
155
+ : (oversize
156
+ ? 'provider 输出超过 ' + MAX_STDOUT_BYTES + ' 字节'
157
+ : 'provider 执行失败:' + (code || proc.error.message || 'unknown')),
158
+ };
159
+ }
160
+ if (proc.status !== 0) {
161
+ return {
162
+ status: 'error',
163
+ provider_id: provider.id,
164
+ duration_ms: duration,
165
+ note: 'provider 退出码非 0:' + proc.status,
166
+ };
167
+ }
168
+ let response;
169
+ try {
170
+ response = JSON.parse(String(proc.stdout || '').trim() || '{}');
171
+ } catch (e) {
172
+ return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是 JSON' };
173
+ }
174
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
175
+ return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是对象' };
176
+ }
177
+ if (response.ok === false) {
178
+ const code = typeof response.code === 'string' && response.code ? response.code : 'provider_error';
179
+ return {
180
+ status: code === 'license_required' ? 'license_required' : 'error',
181
+ provider_id: provider.id,
182
+ code,
183
+ message: typeof response.message === 'string' ? response.message : '',
184
+ duration_ms: duration,
185
+ note: typeof response.message === 'string' ? response.message : code,
186
+ };
187
+ }
188
+ if (response.ok !== true) {
189
+ return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 响应缺 ok 字段' };
190
+ }
191
+ return {
192
+ status: 'active',
193
+ provider_id: provider.id,
194
+ data: response.data,
195
+ duration_ms: duration,
196
+ bytes_out: Buffer.byteLength(String(proc.stdout || ''), 'utf8'),
197
+ note: '',
198
+ };
199
+ }
200
+
201
+ function appendAudit(entry) {
202
+ try {
203
+ const home = providerHome();
204
+ fs.mkdirSync(home, { recursive: true });
205
+ fs.appendFileSync(
206
+ path.join(home, 'provider-audit.jsonl'),
207
+ JSON.stringify(Object.assign({ schema: SCHEMA, ts: new Date().toISOString() }, entry)) + '\n',
208
+ 'utf8',
209
+ );
210
+ } catch (e) {
211
+ // 审计失败不影响调用
212
+ }
213
+ }
214
+
215
+ function runCapability(capability, payload) {
216
+ const config = readConfig();
217
+ if (config.status === 'not_installed') {
218
+ return { status: 'not_installed', provider_id: '', note: '未配置扩展提供方' };
219
+ }
220
+ if (config.status === 'error') {
221
+ return { status: 'error', provider_id: '', note: config.error };
222
+ }
223
+ const provider = pickProvider(config, capability);
224
+ if (!provider) {
225
+ return { status: 'not_installed', provider_id: '', note: '未配置声明 ' + capability + ' 的提供方' };
226
+ }
227
+ const result = invokeProvider(provider, capability, payload);
228
+ appendAudit({
229
+ capability,
230
+ provider_id: result.provider_id,
231
+ status: result.status,
232
+ duration_ms: result.duration_ms,
233
+ bytes_out: result.bytes_out || 0,
234
+ });
235
+ return result;
236
+ }
237
+
238
+ module.exports = {
239
+ SCHEMA,
240
+ DEFAULT_TIMEOUT_MS,
241
+ MAX_TIMEOUT_MS,
242
+ MAX_STDOUT_BYTES,
243
+ KNOWN_CAPABILITIES,
244
+ providerHome,
245
+ configPath,
246
+ readConfig,
247
+ pickProvider,
248
+ runCapability,
249
+ };
@@ -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.20.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', '<关键词>', '按当前任务聚焦', '这次任务很明确、想优先拉相关记忆时', ''),
@@ -7459,30 +7460,55 @@ function cmdWhoami(opts) {
7459
7460
 
7460
7461
  // ---- 用户画像聚合(v0.6.0,零推断:只归组呈现原文,结论由承载 AI 依据「记忆守则」内部形成)----
7461
7462
  function profileGroups(root, owner) {
7462
- const groups = [];
7463
- const map = {};
7463
+ const entries = [];
7464
7464
  for (const t of PRIVATE_LEAF) {
7465
7465
  const dir = path.join(root, PRIVATE_DIR, owner, t);
7466
7466
  if (!fs.existsSync(dir)) continue;
7467
- for (const f of fs.readdirSync(dir)) {
7468
- if (!/\.md(\.enc)?$/.test(f)) continue;
7469
- const fp = path.join(dir, f);
7470
- if (!fs.statSync(fp).isFile()) continue;
7467
+ const files = [];
7468
+ walkEntryFiles(dir, files);
7469
+ for (const fp of files) {
7471
7470
  let e;
7472
7471
  try { e = readEntry(fp, root); } catch (err) { continue; }
7473
- const key = e.type + '\u0000' + (e.subject || '') + '\u0000' + e.tags.join(',');
7474
- if (!map[key]) {
7475
- map[key] = { type: e.type, subject: e.subject || '', tags: e.tags || [], items: [] };
7476
- groups.push(map[key]);
7477
- }
7478
- map[key].items.push({
7479
- file: e.file, statement: e.statement, confidence: e.confidence,
7480
- last_accessed: e.last_accessed, updated: e.updated,
7481
- });
7472
+ entries.push(e);
7482
7473
  }
7483
7474
  }
7475
+ return profileGroupsFromEntries(entries);
7476
+ }
7477
+ function profileGroupsFromEntries(entries) {
7478
+ const groups = [];
7479
+ const map = {};
7480
+ for (const e of (entries || [])) {
7481
+ const key = e.type + '\u0000' + (e.subject || '') + '\u0000' + (e.tags || []).join(',');
7482
+ if (!map[key]) {
7483
+ map[key] = { type: e.type, subject: e.subject || '', tags: e.tags || [], items: [] };
7484
+ groups.push(map[key]);
7485
+ }
7486
+ map[key].items.push({
7487
+ file: e.file, statement: e.statement, confidence: e.confidence,
7488
+ last_accessed: e.last_accessed, updated: e.updated,
7489
+ });
7490
+ }
7484
7491
  return groups;
7485
7492
  }
7493
+ function renderProfileText(owner, groups) {
7494
+ const lines = [];
7495
+ lines.push('# 用户画像(' + owner + ')');
7496
+ lines.push('');
7497
+ lines.push('> 生成: yotta-memory profile | 引擎零推断:以下为私密记忆原文的结构化归组,画像结论由承载 AI 依据「记忆守则」在内部形成,不当面贴标签。');
7498
+ lines.push('> 刷新: ' + today());
7499
+ lines.push('');
7500
+ for (const g of (groups || [])) {
7501
+ lines.push('## ' + g.type + ' · ' + (g.subject || '(无主题)') + (g.tags.length ? ' [tags: ' + g.tags.join(', ') + ']' : ''));
7502
+ lines.push('');
7503
+ for (const it of g.items) {
7504
+ const acc = it.last_accessed ? '最近访问 ' + it.last_accessed : '未访问';
7505
+ lines.push('- ' + it.statement + '(confidence ' + it.confidence + ' · ' + acc + ')');
7506
+ lines.push(' - ' + it.file);
7507
+ }
7508
+ lines.push('');
7509
+ }
7510
+ return lines.join('\n');
7511
+ }
7486
7512
  function profileCore(opts) {
7487
7513
  opts = opts || {};
7488
7514
  const root = userRoot();
@@ -7510,23 +7536,7 @@ function profileCore(opts) {
7510
7536
  }
7511
7537
  const groups = profileGroups(root, owner);
7512
7538
  if (!groups.length) return { error: false, text: '该智能体(' + owner + ')暂无画像:private/' + owner + '/ 下无 PREF / BOUND / COMMIT 条目。' };
7513
- const lines = [];
7514
- lines.push('# 用户画像(' + owner + ')');
7515
- lines.push('');
7516
- lines.push('> 生成: yotta-memory profile | 引擎零推断:以下为私密记忆原文的结构化归组,画像结论由承载 AI 依据「记忆守则」在内部形成,不当面贴标签。');
7517
- lines.push('> 刷新: ' + today());
7518
- lines.push('');
7519
- for (const g of groups) {
7520
- lines.push('## ' + g.type + ' · ' + (g.subject || '(无主题)') + (g.tags.length ? ' [tags: ' + g.tags.join(', ') + ']' : ''));
7521
- lines.push('');
7522
- for (const it of g.items) {
7523
- const acc = it.last_accessed ? '最近访问 ' + it.last_accessed : '未访问';
7524
- lines.push('- ' + it.statement + '(confidence ' + it.confidence + ' · ' + acc + ')');
7525
- lines.push(' - ' + it.file);
7526
- }
7527
- lines.push('');
7528
- }
7529
- const text = lines.join('\n');
7539
+ const text = renderProfileText(owner, groups);
7530
7540
  const outFile = writeProfileText(root, owner, text + '\n');
7531
7541
  return { error: false, text: text + '\n\n[已写入] ' + outFile };
7532
7542
  }
@@ -7561,6 +7571,269 @@ function compareContextRecent(a, b) {
7561
7571
  if (byTime !== 0) return byTime;
7562
7572
  return (b.access_count || 0) - (a.access_count || 0);
7563
7573
  }
7574
+
7575
+ const MEMORY_HOOK_MAX_CANDIDATES = 500;
7576
+ const MEMORY_HOOK_STATEMENT_LIMIT = 2000;
7577
+
7578
+ function contextEntryLine(entry) {
7579
+ return '- [' + entry.type + '] ' + entry.subject + ': ' + entry.statement;
7580
+ }
7581
+
7582
+ function isSelfProfileEntry(entry, owner) {
7583
+ if (!entry || entry.type !== 'PREF' || entry.subject !== SELF_PROFILE_SUBJECT) return false;
7584
+ if (owner && entry.owner && entry.owner !== owner) return false;
7585
+ const file = String(entry.file || '').replace(/\\/g, '/');
7586
+ return owner ? file.indexOf('private/' + owner + '/prefs/') === 0 : false;
7587
+ }
7588
+
7589
+ function contextPageableEntries(readableEntries, owner) {
7590
+ return (readableEntries || [])
7591
+ .filter(function (entry) {
7592
+ return entry.type !== 'BOUND' && entry.type !== 'COMMIT' && !isSelfProfileEntry(entry, owner);
7593
+ })
7594
+ .sort(compareContextRecent);
7595
+ }
7596
+
7597
+ function hookBlock(status, providerId, note) {
7598
+ return {
7599
+ status: status || 'not_installed',
7600
+ provider_id: providerId || '',
7601
+ applied: false,
7602
+ evicted: [],
7603
+ dropped: [],
7604
+ note: note || '',
7605
+ };
7606
+ }
7607
+
7608
+ function hookStatusText(hook) {
7609
+ if (!hook) return '';
7610
+ if (hook.status === 'active') {
7611
+ const who = hook.provider_id ? '提供方 ' + hook.provider_id : '提供方';
7612
+ return '已应用(' + who + ';驱逐 ' + hook.evicted.length + ' / 丢弃 ' + hook.dropped.length + ')';
7613
+ }
7614
+ if (hook.status === 'license_required') return '需授权(该能力需要授权后使用;普通记忆不受影响)';
7615
+ if (hook.status === 'timeout') return '未生效(提供方超时,已回落普通记忆)';
7616
+ if (hook.status === 'invalid_output') return '未生效(提供方输出无效,已回落普通记忆)';
7617
+ if (hook.status === 'error') return '未生效(提供方异常,已回落普通记忆)';
7618
+ return hook.status;
7619
+ }
7620
+
7621
+ function pagingBlock(status, providerId, note) {
7622
+ return {
7623
+ status: status || 'not_installed',
7624
+ provider_id: providerId || '',
7625
+ applied: false,
7626
+ dropped: [],
7627
+ ordered: [],
7628
+ truncated: false,
7629
+ budget: 0,
7630
+ used: 0,
7631
+ note: note || '',
7632
+ };
7633
+ }
7634
+
7635
+ function pagingStatusText(paging) {
7636
+ if (!paging) return '';
7637
+ if (paging.status === 'active') {
7638
+ const who = paging.provider_id ? '提供方 ' + paging.provider_id : '提供方';
7639
+ return '已应用(' + who + ';丢弃 ' + paging.dropped.length + ' / 排序 ' + paging.ordered.length + ')';
7640
+ }
7641
+ if (paging.status === 'license_required') return '需授权(该能力需要授权后使用;普通记忆不受影响)';
7642
+ if (paging.status === 'timeout') return '未生效(提供方超时,已回落普通记忆)';
7643
+ if (paging.status === 'invalid_output') return '未生效(提供方输出无效,已回落普通记忆)';
7644
+ if (paging.status === 'error') return '未生效(提供方异常,已回落普通记忆)';
7645
+ return '';
7646
+ }
7647
+
7648
+ function pagingCandidatePayload(entry, baselineSection) {
7649
+ return {
7650
+ file: entry.file,
7651
+ type: entry.type,
7652
+ subject: entry.subject,
7653
+ statement: String(entry.statement || '').slice(0, MEMORY_HOOK_STATEMENT_LIMIT),
7654
+ created: entry.created || '',
7655
+ updated: entry.updated || '',
7656
+ chars: contextEntryLine(entry).length,
7657
+ baseline_section: baselineSection || 'high_value',
7658
+ };
7659
+ }
7660
+
7661
+ function applyContextPaging(pageable, opts, meta) {
7662
+ opts = opts || {};
7663
+ meta = meta || {};
7664
+ const budget = parseInt(opts.budget || 0, 10) || 0;
7665
+ if (budget <= 0) {
7666
+ return {
7667
+ block: pagingBlock('not_requested', '', '未传 --budget,未调用 context.paging'),
7668
+ drop: new Set(),
7669
+ order: null,
7670
+ applied: false,
7671
+ };
7672
+ }
7673
+ const block = pagingBlock('not_installed', '', '');
7674
+ block.budget = budget;
7675
+ const truncated = pageable.length > MEMORY_HOOK_MAX_CANDIDATES;
7676
+ block.truncated = truncated;
7677
+ const candidates = pageable.slice(0, MEMORY_HOOK_MAX_CANDIDATES);
7678
+ const payload = {
7679
+ agent: opts.owner || '',
7680
+ budget: budget,
7681
+ focus: opts.focus || '',
7682
+ truncated: truncated,
7683
+ protected: Array.isArray(meta.protected) ? meta.protected.slice() : [],
7684
+ sections: meta.sections || {},
7685
+ candidates: candidates.map(function (entry) {
7686
+ return pagingCandidatePayload(entry, (meta.baselineSections || {})[entry.file]);
7687
+ }),
7688
+ };
7689
+ let run;
7690
+ try {
7691
+ run = require('./provider').runCapability('context.paging', payload);
7692
+ } catch (e) {
7693
+ block.status = 'error';
7694
+ block.note = '预算分页失败:' + e.message;
7695
+ return { block: block, drop: new Set(), order: null, applied: false };
7696
+ }
7697
+ block.status = run.status;
7698
+ block.provider_id = run.provider_id || '';
7699
+ block.note = run.note || run.message || '';
7700
+ if (run.status !== 'active' || !run.data || typeof run.data !== 'object') {
7701
+ return { block: block, drop: new Set(), order: null, applied: false };
7702
+ }
7703
+ const sent = new Set(payload.candidates.map(function (item) { return item.file; }));
7704
+ const rejected = [];
7705
+ const dropped = [];
7706
+ for (const item of (Array.isArray(run.data.drop) ? run.data.drop : [])) {
7707
+ const file = String(item || '');
7708
+ if (!file) continue;
7709
+ if (!sent.has(file)) {
7710
+ if (rejected.indexOf(file) === -1) rejected.push(file);
7711
+ continue;
7712
+ }
7713
+ if (dropped.indexOf(file) === -1) dropped.push(file);
7714
+ }
7715
+ let order = null;
7716
+ if (Array.isArray(run.data.order)) {
7717
+ if (run.data.complete === true && !truncated) {
7718
+ const picked = [];
7719
+ for (const item of run.data.order) {
7720
+ const file = String(item || '');
7721
+ if (!file) continue;
7722
+ if (!sent.has(file)) {
7723
+ if (rejected.indexOf(file) === -1) rejected.push(file);
7724
+ continue;
7725
+ }
7726
+ if (picked.indexOf(file) === -1) picked.push(file);
7727
+ }
7728
+ order = picked;
7729
+ } else if (truncated) {
7730
+ block.note = block.note || '候选超过 ' + MEMORY_HOOK_MAX_CANDIDATES + ',order 未应用(保护)';
7731
+ } else {
7732
+ block.note = block.note || 'provider 未声明 complete=true,order 未应用(保护)';
7733
+ }
7734
+ }
7735
+ if (rejected.length) {
7736
+ const text = '候选集外文件已忽略:' + rejected.join(', ');
7737
+ block.note = block.note ? block.note + ';' + text : text;
7738
+ }
7739
+ block.dropped = dropped;
7740
+ block.ordered = order || [];
7741
+ block.applied = dropped.length > 0 || (order !== null && order.length > 0);
7742
+ return { block: block, drop: new Set(dropped), order: order, applied: block.applied };
7743
+ }
7744
+
7745
+ /**
7746
+ * memory.hook 装载点:只把「可驱逐」条目(非 BOUND / COMMIT)交给 provider。
7747
+ * provider 只能返回本次候选集内的 evict(驱逐清单);complete=true 时可选 selected(白名单)。
7748
+ * 身份画像、BOUND / COMMIT、预算 / 去重 / 宽限始终由引擎掌控;任何失败都走普通记忆。
7749
+ * 协议见 references/provider-protocol.md。
7750
+ */
7751
+ function applyMemoryHook(readableEntries, opts) {
7752
+ const block = hookBlock('not_installed', '', '');
7753
+ const pageable = contextPageableEntries(readableEntries, opts.owner || '');
7754
+ const truncated = pageable.length > MEMORY_HOOK_MAX_CANDIDATES;
7755
+ const payload = {
7756
+ agent: opts.owner || '',
7757
+ budget: opts.budget || 0,
7758
+ focus: opts.focus || '',
7759
+ truncated: truncated,
7760
+ candidates: pageable.slice(0, MEMORY_HOOK_MAX_CANDIDATES).map(function (entry) {
7761
+ return {
7762
+ file: entry.file,
7763
+ type: entry.type,
7764
+ subject: entry.subject,
7765
+ statement: String(entry.statement || '').slice(0, MEMORY_HOOK_STATEMENT_LIMIT),
7766
+ created: entry.created || '',
7767
+ updated: entry.updated || '',
7768
+ };
7769
+ }),
7770
+ };
7771
+ let run;
7772
+ try {
7773
+ run = require('./provider').runCapability('memory.hook', payload);
7774
+ } catch (e) {
7775
+ block.status = 'error';
7776
+ block.note = '扩展装载失败:' + e.message;
7777
+ return { block: block, evict: new Set(), selected: null };
7778
+ }
7779
+ block.status = run.status;
7780
+ block.provider_id = run.provider_id || '';
7781
+ block.note = run.note || run.message || '';
7782
+ if (run.status !== 'active' || !run.data || typeof run.data !== 'object') {
7783
+ return { block: block, evict: new Set(), selected: null };
7784
+ }
7785
+ const sent = new Set(payload.candidates.map(function (item) { return item.file; }));
7786
+ const evicted = [];
7787
+ for (const item of (Array.isArray(run.data.evict) ? run.data.evict : [])) {
7788
+ const file = String(item || '');
7789
+ if (!file) continue;
7790
+ if (!sent.has(file)) {
7791
+ block.dropped.push(file);
7792
+ continue;
7793
+ }
7794
+ if (evicted.indexOf(file) === -1) evicted.push(file);
7795
+ }
7796
+ let selected = null;
7797
+ if (Array.isArray(run.data.selected) && run.data.complete === true && !truncated) {
7798
+ const picked = [];
7799
+ for (const item of run.data.selected) {
7800
+ const file = String(item || '');
7801
+ if (!file) continue;
7802
+ if (!sent.has(file)) {
7803
+ block.dropped.push(file);
7804
+ continue;
7805
+ }
7806
+ if (picked.indexOf(file) === -1) picked.push(file);
7807
+ }
7808
+ selected = new Set(picked);
7809
+ }
7810
+ block.evicted = evicted;
7811
+ block.applied = evicted.length > 0 || selected !== null;
7812
+ if (truncated && Array.isArray(run.data.selected)) {
7813
+ block.note = block.note || '候选超过 ' + MEMORY_HOOK_MAX_CANDIDATES + ',白名单模式未应用(保护)';
7814
+ }
7815
+ return { block: block, evict: new Set(evicted), selected: selected };
7816
+ }
7817
+
7818
+ function replaceContextProfileSection(lines, profileText, fence) {
7819
+ const start = lines.indexOf('## 2. 用户画像摘要');
7820
+ if (start === -1) return false;
7821
+ const marker = lines.indexOf('## 2.5 长期理解摘要');
7822
+ const end = marker === -1 ? lines.length : marker;
7823
+ if (end < start) return false;
7824
+ const block = ['## 2. 用户画像摘要', ''];
7825
+ if (profileText) {
7826
+ block.push(fence + 'markdown');
7827
+ block.push(String(profileText).trim());
7828
+ block.push(fence);
7829
+ } else {
7830
+ block.push('(该身份暂无画像;可运行 yotta-memory profile 生成)');
7831
+ }
7832
+ block.push('');
7833
+ lines.splice.apply(lines, [start, end - start].concat(block));
7834
+ return true;
7835
+ }
7836
+
7564
7837
  function contextCore(opts) {
7565
7838
  opts = opts || {};
7566
7839
  const roots = memoryRoots();
@@ -7594,6 +7867,7 @@ function contextCore(opts) {
7594
7867
  const lines = [];
7595
7868
  const shownFiles = new Set();
7596
7869
  const usedFiles = new Set();
7870
+ let usedDynamicChars = 0;
7597
7871
  const FENCE = String.fromCharCode(96, 96, 96);
7598
7872
  function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
7599
7873
  function appendContextEntry(entry, reason, options) {
@@ -7603,7 +7877,7 @@ function contextCore(opts) {
7603
7877
  trace.push('[dropped] ' + entry.file + ' reason: duplicate');
7604
7878
  return false;
7605
7879
  }
7606
- const line = '- [' + entry.type + '] ' + entry.subject + ': ' + entry.statement;
7880
+ const line = contextEntryLine(entry);
7607
7881
  if (options.budget !== false && budget > 0 && usedChars() + line.length > budget) {
7608
7882
  trace.push('[dropped] ' + entry.file + ' reason: budget_exceeded');
7609
7883
  return false;
@@ -7611,6 +7885,7 @@ function contextCore(opts) {
7611
7885
  lines.push(line);
7612
7886
  shownFiles.add(entry.file);
7613
7887
  usedFiles.add(entry.file);
7888
+ usedDynamicChars += line.length;
7614
7889
  trace.push('[included] ' + entry.file + ' reason: ' + reason);
7615
7890
  return true;
7616
7891
  }
@@ -7672,19 +7947,16 @@ function contextCore(opts) {
7672
7947
  }
7673
7948
  }
7674
7949
 
7675
- lines.push('## 2.5 长期理解摘要');
7676
- lines.push('');
7677
- const summaries = readableEntries.filter(isConsolidatedSummary).sort(compareContextRecent);
7678
- const summaryLimit = Math.min(3, Math.max(0, limit));
7679
- if (!summaries.length) lines.push('(暂无周期摘要;可用 yotta-memory consolidate --apply 生成)');
7680
- for (const e of summaries.slice(0, summaryLimit)) {
7681
- appendContextEntry(e, 'summary_priority', { budget: false });
7950
+ const memHook = applyMemoryHook(readableEntries, { owner: owner, budget: budget, focus: focus, limit: limit });
7951
+ const hookState = memHook.block;
7952
+ function hookAllows(entry) {
7953
+ if (!entry) return false;
7954
+ if (memHook.selected && !memHook.selected.has(entry.file)) return false;
7955
+ if (memHook.evict.has(entry.file)) return false;
7956
+ return true;
7682
7957
  }
7683
- lines.push('');
7684
-
7958
+ let focusedEntries = [];
7685
7959
  if (focus) {
7686
- lines.push('## 2.6 任务相关记忆(--focus)');
7687
- lines.push('');
7688
7960
  const focused = recallCore(focus, {
7689
7961
  limit: limit,
7690
7962
  owner: owner,
@@ -7695,9 +7967,126 @@ function contextCore(opts) {
7695
7967
  embeddingTimeout: embeddingTimeout,
7696
7968
  noUsage: true
7697
7969
  });
7698
- const focusedEntries = focused.entries || [];
7970
+ focusedEntries = focused.entries || [];
7971
+ }
7972
+ const pageable = contextPageableEntries(readableEntries, owner).filter(hookAllows);
7973
+ const summaryLimit = Math.min(3, Math.max(0, limit));
7974
+ const summaryCandidates = pageable.filter(isConsolidatedSummary).slice(0, summaryLimit);
7975
+ const summaryFiles = new Set(summaryCandidates.map(function (entry) { return entry.file; }));
7976
+ const pageableFiles = new Set(pageable.map(function (entry) { return entry.file; }));
7977
+ const focusCandidates = focusedEntries.filter(function (entry) {
7978
+ return hookAllows(entry) && pageableFiles.has(entry.file);
7979
+ });
7980
+ const focusFiles = new Set(focusCandidates.map(function (entry) { return entry.file; }));
7981
+ const corridorCandidates = pageable
7982
+ .filter(function (entry) {
7983
+ return !isConsolidatedSummary(entry) && !summaryFiles.has(entry.file) && !focusFiles.has(entry.file);
7984
+ })
7985
+ .slice(0, limit);
7986
+ const corridorFiles = new Set(corridorCandidates.map(function (entry) { return entry.file; }));
7987
+ const highValueCandidates = pageable
7988
+ .filter(function (entry) {
7989
+ return !isConsolidatedSummary(entry) && !summaryFiles.has(entry.file) && !focusFiles.has(entry.file) && !corridorFiles.has(entry.file);
7990
+ })
7991
+ .map(function (entry) { return { e: entry, s: 0.5 * importanceScore(entry) + 0.5 * utilityScore(entry) }; })
7992
+ .sort(function (a, b) { return b.s - a.s; })
7993
+ .slice(0, limit)
7994
+ .map(function (item) { return item.e; });
7995
+ const baselineSections = {};
7996
+ function markBaseline(entries, section) {
7997
+ for (const entry of entries) baselineSections[entry.file] = section;
7998
+ }
7999
+ markBaseline(summaryCandidates, 'summary');
8000
+ markBaseline(focusCandidates, 'focus');
8001
+ markBaseline(corridorCandidates, 'corridor');
8002
+ markBaseline(highValueCandidates, 'high_value');
8003
+ const paging = applyContextPaging(pageable, {
8004
+ owner: owner,
8005
+ budget: budget,
8006
+ focus: focus,
8007
+ limit: limit,
8008
+ }, {
8009
+ protected: readableEntries
8010
+ .filter(function (entry) { return isSelfProfileEntry(entry, owner); })
8011
+ .map(function (entry) { return entry.file; }),
8012
+ sections: {
8013
+ summary: summaryCandidates.map(function (entry) { return entry.file; }),
8014
+ focus: focusCandidates.map(function (entry) { return entry.file; }),
8015
+ corridor: corridorCandidates.map(function (entry) { return entry.file; }),
8016
+ high_value: highValueCandidates.map(function (entry) { return entry.file; }),
8017
+ },
8018
+ baselineSections: baselineSections,
8019
+ });
8020
+ const pagingState = paging.block;
8021
+ const pagingOrder = new Map();
8022
+ for (let i = 0; i < pagingState.ordered.length; i++) pagingOrder.set(pagingState.ordered[i], i);
8023
+ function pagingAllows(entry) {
8024
+ return !entry || !paging.drop.has(entry.file);
8025
+ }
8026
+ function decisionAllows(entry) {
8027
+ return hookAllows(entry) && pagingAllows(entry);
8028
+ }
8029
+ function pagingOrderIndex(entry) {
8030
+ if (!entry || !pagingOrder.has(entry.file)) return Number.MAX_SAFE_INTEGER;
8031
+ return pagingOrder.get(entry.file);
8032
+ }
8033
+ function compareWithPaging(a, b, fallback) {
8034
+ const byOrder = pagingOrderIndex(a) - pagingOrderIndex(b);
8035
+ if (byOrder !== 0) return byOrder;
8036
+ return fallback(a, b);
8037
+ }
8038
+ if ((hookState.applied || pagingState.applied) && owner) {
8039
+ const liveGroups = profileGroupsFromEntries(
8040
+ readableEntries
8041
+ .filter(function (entry) { return entry.type === 'PREF' || entry.type === 'BOUND' || entry.type === 'COMMIT'; })
8042
+ .filter(decisionAllows)
8043
+ );
8044
+ replaceContextProfileSection(lines, renderProfileText(owner, liveGroups), FENCE);
8045
+ trace.push('[profile] source: live reason: ' + (hookState.applied ? 'memory.hook' : 'context.paging'));
8046
+ }
8047
+ const statusLines = [];
8048
+ if (hookState.status !== 'not_installed') {
8049
+ statusLines.push('- 扩展装载(memory.hook): ' + hookStatusText(hookState));
8050
+ trace.push('[hook] status: ' + hookState.status
8051
+ + ' provider: ' + (hookState.provider_id || '-')
8052
+ + ' evicted: ' + hookState.evicted.length
8053
+ + ' dropped: ' + hookState.dropped.length);
8054
+ }
8055
+ const pagingText = pagingStatusText(pagingState);
8056
+ if (pagingText) {
8057
+ statusLines.push('- 预算分页(context.paging): ' + pagingText);
8058
+ trace.push('[paging] status: ' + pagingState.status
8059
+ + ' provider: ' + (pagingState.provider_id || '-')
8060
+ + ' dropped: ' + pagingState.dropped.length
8061
+ + ' ordered: ' + pagingState.ordered.length
8062
+ + ' truncated: ' + (pagingState.truncated ? 'true' : 'false'));
8063
+ }
8064
+ if (statusLines.length) {
8065
+ const anchor = lines.indexOf('## 1. 身份');
8066
+ statusLines.push('');
8067
+ lines.splice.apply(lines, [anchor === -1 ? 2 : anchor, 0].concat(statusLines));
8068
+ }
8069
+
8070
+ lines.push('## 2.5 长期理解摘要');
8071
+ lines.push('');
8072
+ const summaries = readableEntries
8073
+ .filter(isConsolidatedSummary)
8074
+ .filter(decisionAllows)
8075
+ .sort(function (a, b) { return compareWithPaging(a, b, compareContextRecent); });
8076
+ if (!summaries.length) lines.push('(暂无周期摘要;可用 yotta-memory consolidate --apply 生成)');
8077
+ for (const e of summaries.slice(0, summaryLimit)) {
8078
+ appendContextEntry(e, 'summary_priority', { budget: false });
8079
+ }
8080
+ lines.push('');
8081
+
8082
+ if (focus) {
8083
+ lines.push('## 2.6 任务相关记忆(--focus)');
8084
+ lines.push('');
7699
8085
  if (!focusedEntries.length) lines.push('(无匹配记忆)');
7700
- for (const e of focusedEntries) {
8086
+ const focusedVisible = focusedEntries
8087
+ .filter(decisionAllows)
8088
+ .sort(function (a, b) { return compareWithPaging(a, b, function () { return 0; }); });
8089
+ for (const e of focusedVisible) {
7701
8090
  appendContextEntry(e, 'focus_match score: ' + round3(e.score));
7702
8091
  }
7703
8092
  lines.push('');
@@ -7707,7 +8096,8 @@ function contextCore(opts) {
7707
8096
  lines.push('');
7708
8097
  const corridor = readableEntries
7709
8098
  .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT'; })
7710
- .sort(compareContextRecent)
8099
+ .filter(decisionAllows)
8100
+ .sort(function (a, b) { return compareWithPaging(a, b, compareContextRecent); })
7711
8101
  .filter(function (e) { return !shownFiles.has(e.file); })
7712
8102
  .slice(0, limit);
7713
8103
  if (!corridor.length) lines.push('(暂无近期记忆)');
@@ -7717,9 +8107,9 @@ function contextCore(opts) {
7717
8107
  lines.push('## 4. 近期高价值记忆(补位)');
7718
8108
  lines.push('');
7719
8109
  const highValue = readableEntries
7720
- .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file); })
8110
+ .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file) && decisionAllows(e); })
7721
8111
  .map(function (e) { return { e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) }; })
7722
- .sort(function (a, b) { return b.s - a.s; })
8112
+ .sort(function (a, b) { return compareWithPaging(a.e, b.e, function (x, y) { return y.s - x.s; }); })
7723
8113
  .slice(0, limit);
7724
8114
  if (!highValue.length) lines.push('(暂无补位条目)');
7725
8115
  for (const h of highValue) appendContextEntry(h.e, 'high_value_backfill');
@@ -7777,12 +8167,17 @@ function contextCore(opts) {
7777
8167
  for (const message of reliability.warnings) lines.push('- [警告] ' + message);
7778
8168
  if (!reliability.ok) lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
7779
8169
  }
7780
- return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace };
8170
+ pagingState.used = usedDynamicChars;
8171
+ return { error: false, exitCode: 0, text: lines.join('\n'), trace: trace, hook: hookState, paging: pagingState };
7781
8172
  }
7782
8173
  function cmdContext(opts) {
7783
8174
  const r = contextCore(opts);
7784
- if (opts.json && r.report) console.log(JSON.stringify(r.report, null, 2));
7785
- else console.log(r.text);
8175
+ if (opts.json) {
8176
+ if (r.report) console.log(JSON.stringify(r.report, null, 2));
8177
+ else console.log(JSON.stringify({ schema: 1, hook: r.hook || null, paging: r.paging || null, text: r.text }, null, 2));
8178
+ } else {
8179
+ console.log(r.text);
8180
+ }
7786
8181
  if (r.error) {
7787
8182
  const reminder = migrationReminderText(userRoot());
7788
8183
  if (reminder) console.log('\n' + reminder);
@@ -11078,6 +11473,7 @@ module.exports = {
11078
11473
  profileCore: profileCore,
11079
11474
  contextCore: contextCore,
11080
11475
  contextAuditCore: contextAuditCore,
11476
+ applyMemoryHook: applyMemoryHook,
11081
11477
  importanceScore: importanceScore,
11082
11478
  usageSettings: usageSettings,
11083
11479
  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.20.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,171 @@
1
+ # 扩展提供方协议 v1.1(元忆 · capability `memory.hook` / `context.paging`)
2
+
3
+ 元忆可以可选地调用一个由用户显式配置的**本地扩展提供方(provider)**,由它参与「哪些记忆进上下文」。
4
+ 未配置提供方时,`context` 的行为与输出与之前完全一致;任何失败都回落到普通记忆,不阻断命令。
5
+
6
+ 本版协议包含两个 capability:
7
+
8
+ - `memory.hook`:在装配前过滤候选集(`evict` / `selected`)。
9
+ - `context.paging`:在用户显式传 `--budget` 时,对已通过 `memory.hook` 的候选集做排序与分页(`drop` / `order`)。
10
+
11
+ ## 1. 配置
12
+
13
+ 配置文件:`<YOTTA_PROVIDER_HOME>/provider.json`,默认 `~/.yottameta/provider.json`。
14
+ 环境变量 `YOTTA_PROVIDER_HOME` 可覆盖根目录(测试与隔离环境用)。
15
+
16
+ ```json
17
+ {
18
+ "schema": 1,
19
+ "providers": [
20
+ {
21
+ "id": "local-provider",
22
+ "version": "0.1.0",
23
+ "capabilities": ["memory.hook", "context.paging"],
24
+ "command": ["node", "C:/path/to/provider.js"],
25
+ "timeout_ms": 600
26
+ }
27
+ ]
28
+ }
29
+ ```
30
+
31
+ - `command` 必须是**数组**(argv 语义),以 `shell: false` 执行;不接受字符串命令。
32
+ - `timeout_ms` 默认 600,最小 50,最大 5000;超时即回落。
33
+ - 配置文件缺失、解析失败、`command` 非数组、capability 未知:**只记录状态,不阻断命令**;缺失等同「未安装」。
34
+
35
+ ## 2. 调用
36
+
37
+ - 只在用户显式执行 `context`(或 `context --json` / `--explain`)时触发;`init` / `install` / `doctor` / `scan` 等路径不触发。
38
+ - 元忆把**一个 JSON 请求**写入 provider 的 stdin(随后关闭),从 stdout 读**一个 JSON 响应**;stderr 只作诊断。
39
+ - stdout 上限 256 KB;超过按错误处理并回落。
40
+ - 请求与环境:
41
+
42
+ ```json
43
+ {
44
+ "schema": 1,
45
+ "capability": "memory.hook",
46
+ "request_id": "<uuid>",
47
+ "payload": {
48
+ "agent": "codex",
49
+ "budget": 0,
50
+ "focus": "",
51
+ "truncated": false,
52
+ "candidates": [
53
+ {
54
+ "file": "facts/2026/09/2026-09-27-0001.md",
55
+ "type": "FACT",
56
+ "subject": "示例主题",
57
+ "statement": "示例内容(最多 2000 字)",
58
+ "created": "2026-09-27",
59
+ "updated": "2026-09-27"
60
+ }
61
+ ]
62
+ }
63
+ }
64
+ ```
65
+
66
+ - `candidates` 只包含**调用者有权读取**、且**可驱逐**的条目(已过滤私密越权项;BOUND / COMMIT 不在其中)。
67
+ - 候选最多 500 条;超过时 `truncated = true`,此时白名单模式不生效(见 §3),驱逐模式仍安全。
68
+
69
+ ## 3. 响应
70
+
71
+ ```json
72
+ { "ok": true, "capability": "memory.hook", "data": { "evict": ["facts/2026/09/2026-09-27-0001.md"] } }
73
+ ```
74
+
75
+ - **`evict`(推荐)**:要从上下文里驱逐的条目清单。每个 `file` 必须出现在本次 `candidates` 中;候选集外的 file 会被丢弃并记入 `dropped`。未见过的条目默认保留 —— 候选很多时也安全。
76
+ - **`selected`(可选白名单)**:仅在 `complete: true` 且本次未 `truncated` 时接受。每个 `file` 必须 ∈ `candidates`;未列入的候选会被驱逐。
77
+ - 元忆侧仍强制执行:BOUND / COMMIT 与自我接入档案不可驱逐;`## 1. 身份` 段落不受影响,画像中的其它条目随条目一并参与驱逐;预算、去重、宽限、章节顺序由引擎决定。
78
+
79
+ 需要授权或不可用时:
80
+
81
+ ```json
82
+ { "ok": false, "code": "license_required", "message": "该能力需要授权后使用" }
83
+ ```
84
+
85
+ ## 4. 状态与回落
86
+
87
+ | 状态 | 触发 | 元忆行为 |
88
+ | --- | --- | --- |
89
+ | `not_installed` | 无配置 / 无匹配 capability | 普通记忆,输出与历史一致 |
90
+ | `active` | 调用成功且响应合法 | 在约束内应用 `evict` / `selected` |
91
+ | `license_required` | provider 明确返回该 code | 普通记忆 + 一行状态提示 |
92
+ | `timeout` | 超过 `timeout_ms` | 普通记忆 + 一行状态提示 |
93
+ | `invalid_output` | 非 JSON / 缺 `ok` | 普通记忆 + 一行状态提示 |
94
+ | `error` | 启动失败 / 退出码非 0 / 输出超限 / 配置非法 | 普通记忆 + 一行状态提示 |
95
+
96
+ `context --json` 的 `hook` 块给出 `status` / `provider_id` / `applied` / `evicted` / `dropped` / `note`;
97
+ `context --explain` 的 trace 里追加一行 `[hook] ...`。
98
+
99
+ ## 5. `context.paging`(v1.1 新增)
100
+
101
+ ### 5.1 调用时机
102
+
103
+ - 仅在用户显式执行 `context` 且传了 `--budget`(大于 0)时调用;不传预算不调用,状态为 `not_requested`。
104
+ - 调用顺序:`memory.hook` 先过滤,`context.paging` 再排序 / 分页。
105
+ - 候选集与 `memory.hook` 同源:权限过滤后、去掉 BOUND / COMMIT / 自我接入档案;最多 500 条。
106
+ - `context.paging` 不能覆盖引擎硬约束:身份段落、自我接入档案、BOUND / COMMIT 不参与;最终字符预算仍由元忆引擎强制。
107
+
108
+ ### 5.2 请求
109
+
110
+ ```json
111
+ {
112
+ "schema": 1,
113
+ "capability": "context.paging",
114
+ "request_id": "<uuid>",
115
+ "payload": {
116
+ "agent": "codex",
117
+ "budget": 4000,
118
+ "focus": "",
119
+ "truncated": false,
120
+ "protected": ["private/codex/prefs/2026-08-25-0001.md"],
121
+ "sections": {
122
+ "summary": ["<file>"],
123
+ "focus": ["<file>"],
124
+ "corridor": ["<file>"],
125
+ "high_value": ["<file>"]
126
+ },
127
+ "candidates": [
128
+ {
129
+ "file": "facts/2026/09/2026-09-29-0001.md",
130
+ "type": "FACT",
131
+ "subject": "示例主题",
132
+ "statement": "示例内容(最多 2000 字)",
133
+ "created": "2026-09-29",
134
+ "updated": "2026-09-29",
135
+ "chars": 128,
136
+ "baseline_section": "corridor"
137
+ }
138
+ ]
139
+ }
140
+ }
141
+ ```
142
+
143
+ - `sections` 是引擎基线的分段落位,供 provider 参考,不构成约束。
144
+ - `protected` 列出引擎硬保护、不可驱逐的文件(如自我接入档案)。
145
+
146
+ ### 5.3 响应
147
+
148
+ ```json
149
+ { "ok": true, "capability": "context.paging", "data": {
150
+ "drop": ["<file>"],
151
+ "order": ["<file>", "<file>"],
152
+ "complete": true
153
+ } }
154
+ ```
155
+
156
+ - `drop`:明确分页出去的条目,任意子集,**始终应用**;候选集外文件只记 note,不进入输出。
157
+ - `order`:期望的进入顺序,**仅在 `complete: true` 且本次未 `truncated` 时应用**;未列入的候选按引擎基线顺序补尾,不丢内容。
158
+ - 候选超过 500(`truncated=true`)时只接受 `drop`,`order` 不应用并写入 note。
159
+
160
+ ### 5.4 状态与回落
161
+
162
+ `context --json` 的 `paging` 块给出 `status` / `provider_id` / `applied` / `dropped` / `ordered` / `truncated` / `budget` / `used` / `note`;
163
+ `context --explain` 的 trace 里追加一行 `[paging] ...`。未配置 / 未声明该 capability 时为 `not_installed`;未传 `--budget` 时为 `not_requested`;授权失败 / 超时 / 非法输出 / 异常均回落普通上下文,不影响既有记忆。
164
+
165
+ ## 6. 审计与边界
166
+
167
+ - 每次实际调用写一行 `<YOTTA_PROVIDER_HOME>/provider-audit.jsonl`:`ts` / `capability` / `provider_id` / `status` / `duration_ms` / `bytes_out`。
168
+ - 审计**不记录**记忆正文、查询原文或任何 payload 内容。
169
+ - 元忆不替 provider 联网;provider 自身行为由它自己的包声明。
170
+ - 删除 `provider.json` 即回到普通记忆,无残留依赖。
171
+ - 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.20.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",