@yottameta/yotta-memory 0.23.1 → 0.23.3

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,15 @@
1
+ # Changelog
2
+
3
+ ## v0.23.3 (2026-11-05)
4
+
5
+ - 质量门禁与打包文档修正;发布前测试接入。
6
+ - 发布门禁批次:发布工作流在 `npm publish` 前执行版本/发布面检查与 `npm test`;测试版本断言改为从 `package.json` 读取;补齐测试脚本。
7
+ - 打包与文档:发布包内容、CHANGELOG / README 与交付说明对齐;规则摘要与注册清单同步。
8
+
9
+ ## v0.23.2 (2026-11-01)
10
+
11
+ - 安装器 containment:目标根 / 嵌套链接 fail-closed;受管 Hub 链接解析真实目标;精简安装器同口径。
12
+
1
13
  ## v0.23.1 (2026-10-25)
2
14
 
3
15
  - **ClawHub 英文门面(无引擎变更)**:发布副本改为英文优先的双语 `summary` / `description`(英文在前、中文关键词保留在后)并补英文 tags,上线后 ClawHub 技能卡摘要由纯中文变为英文优先;构建与校验由独立发布副本流程覆盖,中文检索优化字段只增不减。
package/README.md CHANGED
@@ -32,7 +32,7 @@
32
32
  ## Docs and release notes
33
33
 
34
34
  > 📖 The user-facing operations manual lives in [USER_GUIDE.md](USER_GUIDE.md).
35
- > 🆕 **v0.23.1 (ClawHub storefront)**: the ClawHub listing now ships an English-first bilingual summary and description (a dedicated `clawhub-dist/yotta-memory` publish copy), and the README gained the three usage cases above. Commands, options, output and the memory store format are unchanged.
35
+ > 🆕 **v0.23.2 (ClawHub storefront)**: the ClawHub listing now ships an English-first bilingual summary and description (a dedicated `clawhub-dist/yotta-memory` publish copy), and the README gained the three usage cases above. Commands, options, output and the memory store format are unchanged.
36
36
  > 🆕 **v0.18.1 (distillation provenance + absolute dates + consolidation markers + rule-promotion suggestions + write discipline)**: `distill` now emits typed extraction lists (event / lesson / todo / growth / boundary-rule / other) with per-item `[source: file#L<a>-L<b>]` anchors, measured quality metrics and a skipped/boundary list, plus structured `--json`; `consolidate` rewrites relative dates ("yesterday / last week / this month / this year") into absolute dates based on each entry's `created` date and appends a consolidation marker to archived copies (`--undo` strips it); `maintain --rules` prints read-only "repeat pitfalls → promote to a rule" suggestions (default threshold 3, never writes BOUND automatically); the Memory Doctrine gained a 6-level conflict authority order and three write-discipline rules.
37
37
 
38
38
  > 🆕 **v0.18.0 (hit tracking + capacity water level + consolidate gate + context audit)**: `recall` / `explain` / `feedback --useful` / `context` record local hit metadata (`hit_days`, retained 90 days by default; `hit_queries` stores only an 8-hex query fingerprint, up to 12 slots, never the query text) inside the memory file, so it travels with encryption / backup / export. Disable globally with `config set usage_enabled false` or per call with `--no-usage`; read-only commands and cross-owner private entries never write hits. New read-only `maintain --capacity [--json]` reports water level, 30/90-day activity, LRU/LFU eviction candidates (30-day cooldown; immutable / BOUND / `evergreen` / `pinned` exempt) and promotion suggestions (≥3 hits across ≥3 distinct queries; commands only, nothing is changed automatically). `archive` respects cooldown and evergreen tags, with `--force` to override those two exemptions. `consolidate` now defaults to `--propose` (structured report + `--json`); `--apply` requires an interactive confirmation token, or explicit `--yes` in non-interactive runs, and shows a one-time data-lifecycle notice. `doctor` scale checks gained `scale_info_*` thresholds and per-metric `ok / info / warning` levels (`doctor.ok` still only follows critical issues). New `context --audit [--from <file|->] [--json] [--gate N]` checks whether compressed-away content was persisted, printing missing items and `remember` suggestions without writing anything. `archive --dry-run` now truly previews (no file moves, no snapshot, no audit), a no-candidate run no longer takes a whole-store snapshot, and `archive --json` emits a structured report. On the MCP side this release adds read-only / preview inputs only (`archive.dryRun`, `maintain.capacity`, `context.audit` with inline `auditText`, and a propose-only `consolidate` tool), while `archive --force` and `consolidate --apply / --undo` stay CLI-only. Previous v0.17.4: `rename` removed flat/layered same-sequence collisions (cross-layout fail-closed, destructive guard, audit).
package/README.zh-CN.md CHANGED
@@ -22,7 +22,7 @@
22
22
  </p>
23
23
 
24
24
  > 📖 面向用户的操作手册见 [USER_GUIDE.md](USER_GUIDE.md)。
25
- > 🆕 **v0.23.1(ClawHub 门面)**:ClawHub 上的技能卡改为英文优先的双语摘要与描述(新增 `clawhub-dist/yotta-memory` 专属发布副本),README 首屏新增上面三条使用案例;命令、参数、输出与记忆库格式全部不变。
25
+ > 🆕 **v0.23.2(ClawHub 门面)**:ClawHub 上的技能卡改为英文优先的双语摘要与描述(新增 `clawhub-dist/yotta-memory` 专属发布副本),README 首屏新增上面三条使用案例;命令、参数、输出与记忆库格式全部不变。
26
26
  > 🆕 **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`。
27
27
  > 🆕 **v0.18.1(蒸馏溯源 + 日期绝对化 + 巩固标记 + 规则晋升建议 + 写入纪律)**:`distill` 新增分类型提取清单(事件 / 教训 / 待办 / 成长 / 规则边界 / 其他)+ 逐条 `[溯源: 文件#L<a>-L<b>]` + 实测质量指标 + 跳过与边界,`--json` 输出结构化报告;`consolidate` 摘要按条目 `created` 把「昨天 / 上周 / 本月 / 今年」写成绝对日期,归档副本写入巩固标记(`--undo` 剥离还原);`maintain --rules` 只读给出「重复踩坑 → 建议升级为规则」(默认阈值 3,不自动写 BOUND);记忆守则新增冲突权威顺序 6 层与写入纪律 3 条。
28
28
 
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.23.1
4
+ version: 0.23.3
5
5
  license: MIT
6
6
  ---
7
7
 
package/bin/install.js CHANGED
@@ -108,7 +108,12 @@ function skillVersion() {
108
108
  const data = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
109
109
  return data.version || null;
110
110
  } catch (_) {
111
- return null;
111
+ try {
112
+ const version = fs.readFileSync(path.join(PKG_ROOT, 'VERSION'), 'utf8').trim();
113
+ return version || null;
114
+ } catch (_) {
115
+ return null;
116
+ }
112
117
  }
113
118
  }
114
119
 
@@ -178,6 +183,63 @@ function assertSafeTarget(target) {
178
183
  if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
179
184
  throw new UsageError('目标目录不能在技能源目录内(防止自装自毁)');
180
185
  }
186
+ if (isDirectLink(target) && !isManagedHubTarget(target)) {
187
+ throw new TargetError('目标目录是符号链接或 junction,拒绝跟随: ' + target);
188
+ }
189
+ }
190
+
191
+ function isDirectLink(target) {
192
+ let stat = null;
193
+ try { stat = fs.lstatSync(target); } catch (_) { return false; }
194
+ if (stat.isSymbolicLink()) return true;
195
+ try {
196
+ const real = fs.realpathSync(target);
197
+ const parentReal = fs.realpathSync(path.dirname(target));
198
+ const norm = (value) => process.platform === 'win32'
199
+ ? path.resolve(value).toLowerCase()
200
+ : path.resolve(value);
201
+ return norm(real) !== norm(path.join(parentReal, path.basename(target)));
202
+ } catch (_) {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ function isManagedHubTarget(target) {
208
+ if (!isDirectLink(target)) return false;
209
+ try {
210
+ const real = fs.realpathSync(target);
211
+ if (path.basename(real) !== SKILL_NAME) return false;
212
+ const parent = path.dirname(real);
213
+ return fs.existsSync(path.join(parent, '.yotta-hub.json'))
214
+ || fs.existsSync(path.join(parent, '.yotta-links.json'));
215
+ } catch (_) {
216
+ return false;
217
+ }
218
+ }
219
+
220
+ function removeLink(target) {
221
+ try {
222
+ fs.unlinkSync(target);
223
+ } catch (_) {
224
+ fs.rmdirSync(target);
225
+ }
226
+ }
227
+
228
+ function safeRemove(target) {
229
+ let stat = null;
230
+ try { stat = fs.lstatSync(target); } catch (_) { return; }
231
+ if (isDirectLink(target)) {
232
+ removeLink(target);
233
+ return;
234
+ }
235
+ if (stat.isDirectory()) {
236
+ for (const name of fs.readdirSync(target)) {
237
+ safeRemove(path.join(target, name));
238
+ }
239
+ fs.rmdirSync(target);
240
+ } else {
241
+ fs.unlinkSync(target);
242
+ }
181
243
  }
182
244
 
183
245
  function shouldSkipCache(name, isFile) {
@@ -195,9 +257,18 @@ function copyDir(src, dst, topLevel) {
195
257
  const s = path.join(src, entry.name);
196
258
  const d = path.join(dst, entry.name);
197
259
  if (entry.isDirectory()) {
260
+ if (isDirectLink(d)) {
261
+ throw new TargetError('目标目录包含符号链接或 junction,拒绝写入: ' + d);
262
+ }
198
263
  fs.mkdirSync(d, { recursive: true });
264
+ if (isDirectLink(d)) {
265
+ throw new TargetError('目标目录包含符号链接或 junction,拒绝写入: ' + d);
266
+ }
199
267
  copyDir(s, d, false);
200
268
  } else if (entry.isFile()) {
269
+ if (isDirectLink(d)) {
270
+ throw new TargetError('目标文件是符号链接,拒绝写入: ' + d);
271
+ }
201
272
  fs.copyFileSync(s, d);
202
273
  }
203
274
  }
@@ -208,16 +279,18 @@ function copyDir(src, dst, topLevel) {
208
279
  // 不整目录删除、不跟随符号链接。
209
280
  function cleanResidue(target) {
210
281
  const removed = [];
282
+ const workTarget = isManagedHubTarget(target) ? fs.realpathSync(target) : target;
211
283
  let stat = null;
212
- try { stat = fs.statSync(target); } catch (_) { return removed; }
284
+ try { stat = fs.lstatSync(workTarget); } catch (_) { return removed; }
213
285
  if (!stat.isDirectory()) return removed;
214
- if (!fs.existsSync(path.join(target, 'SKILL.md'))) return removed;
286
+ if (isDirectLink(workTarget)) return removed;
287
+ if (!fs.existsSync(path.join(workTarget, 'SKILL.md'))) return removed;
215
288
  for (const name of TOP_SKIP) {
216
- const p = path.join(target, name);
289
+ const p = path.join(workTarget, name);
217
290
  let entry = null;
218
291
  try { entry = fs.lstatSync(p); } catch (_) { continue; }
219
- if (entry.isSymbolicLink()) continue;
220
- fs.rmSync(p, { recursive: true, force: true });
292
+ if (entry.isSymbolicLink() || isDirectLink(p)) continue;
293
+ safeRemove(p);
221
294
  removed.push(name);
222
295
  }
223
296
  try {
@@ -233,10 +306,10 @@ function cleanResidue(target) {
233
306
  const walk = (dir) => {
234
307
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
235
308
  const p = path.join(dir, entry.name);
236
- if (entry.isSymbolicLink()) continue;
309
+ if (entry.isSymbolicLink() || isDirectLink(p)) continue;
237
310
  if (entry.isDirectory()) {
238
311
  if (CACHE_DIRS.has(entry.name)) {
239
- fs.rmSync(p, { recursive: true, force: true });
312
+ safeRemove(p);
240
313
  removed.push(path.relative(target, p));
241
314
  } else {
242
315
  walk(p);
@@ -247,7 +320,7 @@ function cleanResidue(target) {
247
320
  }
248
321
  }
249
322
  };
250
- walk(target);
323
+ walk(workTarget);
251
324
  return removed;
252
325
  }
253
326
 
@@ -255,13 +328,6 @@ function installTo(dest, opts) {
255
328
  if (!dest || typeof dest !== 'string') throw new UsageError('目标目录不能为空');
256
329
  const target = path.resolve(dest, SKILL_NAME);
257
330
  assertSafeTarget(target);
258
- // fail-closed:目标基目录 / 技能目录是符号链接(含 Windows junction)时拒绝跟随;
259
- // 与 install.sh 同口径,避免安装器改写 Hub 链接指向的真源(含 runtimePayload)。
260
- for (const p of [path.resolve(dest), target]) {
261
- let st = null;
262
- try { st = fs.lstatSync(p); } catch (_) { continue; }
263
- if (st.isSymbolicLink()) throw new TargetError('目标路径是符号链接,拒绝跟随:' + p);
264
- }
265
331
  if (opts.dryRun) {
266
332
  console.log('[dry-run] 将安装到 -> ' + target);
267
333
  return target;
@@ -269,6 +335,9 @@ function installTo(dest, opts) {
269
335
  try {
270
336
  for (const rel of cleanResidue(target)) console.log('已清理残留: ' + rel);
271
337
  fs.mkdirSync(target, { recursive: true });
338
+ if (isDirectLink(target) && !isManagedHubTarget(target)) {
339
+ throw new TargetError('目标目录在安装期间变成了链接,拒绝写入: ' + target);
340
+ }
272
341
  copyDir(PKG_ROOT, target, true);
273
342
  if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
274
343
  throw new InstallError('安装结果缺少 SKILL.md');
@@ -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.23.1';
28
+ const VERSION = '0.23.3';
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
 
package/install.sh CHANGED
@@ -125,6 +125,25 @@ assert_safe_target() {
125
125
  return 0
126
126
  }
127
127
 
128
+ is_managed_hub_link() {
129
+ local p="$1" real parent
130
+ [ -L "$p" ] || return 1
131
+ real="$(resolve_path "$p")" || return 1
132
+ [ "$(basename "$real")" = "$SKILL_NAME" ] || return 1
133
+ parent="$(dirname "$real")"
134
+ [ -f "$parent/.yotta-hub.json" ] || [ -f "$parent/.yotta-links.json" ]
135
+ }
136
+
137
+ assert_no_nested_links() {
138
+ local dest="$1"
139
+ [ -d "$dest" ] || return 0
140
+ if find "$dest" -mindepth 1 -type l -print -quit 2>/dev/null | grep -q .; then
141
+ echo "安装失败:目标目录包含符号链接,拒绝写入:$dest" >&2
142
+ return 1
143
+ fi
144
+ return 0
145
+ }
146
+
128
147
  # 清理旧版安装残留(fail-closed 白名单):仅在目标目录已存在且含 SKILL.md 时触发;
129
148
  # 只删顶层开发项 + 任意层级缓存;不整目录删除、不跟随符号链接。
130
149
  DEV_SKIP="package.json package-lock.json bin lib test .github .git .gitignore .npmignore .gitattributes .yotta .tmp install.sh node_modules test-fixtures"
@@ -154,7 +173,15 @@ clean_residue() {
154
173
  # 任意层级跳过缓存(__pycache__ / .pytest_cache / .mypy_cache / *.pyc / *.pyo)。
155
174
  copy_tree() {
156
175
  local src="$1" dst="$2" top="$3" entry base
157
- mkdir -p "$dst"
176
+ if [ "$top" != "1" ] && [ -L "$dst" ]; then
177
+ echo "安装失败:目标目录包含符号链接,拒绝写入:$dst" >&2
178
+ return 1
179
+ fi
180
+ mkdir -p "$dst" || return 1
181
+ if [ "$top" != "1" ] && [ -L "$dst" ]; then
182
+ echo "安装失败:目标目录包含符号链接,拒绝写入:$dst" >&2
183
+ return 1
184
+ fi
158
185
  for entry in "$src"/* "$src"/.[!.]* "$src"/..?*; do
159
186
  [ -e "$entry" ] || [ -L "$entry" ] || continue
160
187
  base="$(basename "$entry")"
@@ -170,11 +197,19 @@ copy_tree() {
170
197
  esac
171
198
  fi
172
199
  if [ -d "$entry" ] && [ ! -L "$entry" ]; then
200
+ if [ -L "$dst/$base" ]; then
201
+ echo "安装失败:目标目录包含符号链接,拒绝写入:$dst/$base" >&2
202
+ return 1
203
+ fi
173
204
  copy_tree "$entry" "$dst/$base" 0 || return 1
174
205
  else
175
206
  case "$base" in
176
207
  *.pyc|*.pyo) continue ;;
177
208
  esac
209
+ if [ -L "$dst/$base" ]; then
210
+ echo "安装失败:目标文件是符号链接,拒绝写入:$dst/$base" >&2
211
+ return 1
212
+ fi
178
213
  cp -RP "$entry" "$dst/$base" || return 1
179
214
  fi
180
215
  done
@@ -191,7 +226,7 @@ install_to() {
191
226
  echo "安装失败:目标路径已存在且不是目录:$base" >&2
192
227
  return 1
193
228
  fi
194
- if [ -L "$dest" ]; then
229
+ if [ -L "$dest" ] && ! is_managed_hub_link "$dest"; then
195
230
  echo "安装失败:技能目录是符号链接,拒绝跟随:$dest" >&2
196
231
  return 1
197
232
  fi
@@ -200,12 +235,17 @@ install_to() {
200
235
  return 1
201
236
  fi
202
237
  assert_safe_target "$dest" || return 1
238
+ assert_no_nested_links "$dest" || return 1
203
239
  if [ "$dry" = "1" ]; then
204
240
  echo "[dry-run] 将安装到 -> $dest"
205
241
  return 0
206
242
  fi
207
243
  clean_residue "$dest"
208
244
  mkdir -p "$dest" || return 1
245
+ if [ -L "$dest" ] && ! is_managed_hub_link "$dest"; then
246
+ echo "安装失败:技能目录在安装期间变成了链接,拒绝写入:$dest" >&2
247
+ return 1
248
+ fi
209
249
  copy_tree "$SOURCE_DIR" "$dest" 1 || return 1
210
250
  if [ ! -f "$dest/SKILL.md" ]; then
211
251
  echo "安装失败:结果缺少 SKILL.md" >&2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-memory",
3
- "version": "0.23.1",
3
+ "version": "0.23.3",
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": [
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-memory",
4
4
  "name": "元忆",
5
5
  "package": "@yottameta/yotta-memory",
6
- "version": "0.23.1",
6
+ "version": "0.23.3",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true