memory-eternal 0.9.15 → 0.9.16

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memory-eternal",
4
- "version": "0.9.15",
4
+ "version": "0.9.16",
5
5
  "description": "璁板繂鏍稿績锛圡emory Eternal锛夛細鐭ヨ瘑鍗¤嚜鍔ㄦ矇娣€/鍙洖锛岀粺涓€ ~/.dsh/memory-vault锛屼細璇濈粨鏉熻嚜鍔ㄥ叆搴撹繘寰呭鏍搞€?,
6
6
  "author": {
7
7
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "璁板繂鏍稿績锛圡emory Eternal锛夛細鐭ヨ瘑鍗¤嚜鍔ㄦ矇娣€/鍙洖锛岀粺涓€ ~/.dsh/memory-vault锛屼細璇濈粨鏉熻嚜鍔ㄥ叆搴撹繘寰呭鏍搞€?,
5
5
  "author": {
6
6
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "璁板繂鏍稿績锛圡emory Eternal锛夛細鐭ヨ瘑鍗¤嚜鍔ㄦ矇娣€/鍙洖锛岀粺涓€ ~/.dsh/memory-vault锛屼細璇濈粨鏉熻嚜鍔ㄥ叆搴撹繘寰呭鏍搞€?,
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.en.md CHANGED
@@ -312,6 +312,7 @@ Diagnostics:
312
312
 
313
313
  | Version | Date | Highlights |
314
314
  |---|---|---|
315
+ | **v0.9.16** | 2026-09-30 | **Fixed config saves silently not persisting (issue #12) + full config coverage.** ① **Save path**: the host `dsh-settings.write()` requires the revision held by the panel to match `describe()` **exactly**; once stale it throws `SettingsConflictError` and the save fails — the plugin now re-reads the latest revision and **retries once** (marked ♻ in the message). ② **Late volatile echo**: after a successful write the patch is overlaid on the local view (reopening the panel shows the new value immediately) and the overlay is dropped once the host snapshot catches up; the host POST now **re-reads after writing** and returns `pending` with an honest note ("written; restart DSH to apply from the config file") when the echo has not landed. ③ **Immediate save feedback** (requested earlier): ⏳ Saving / ✅ Saved / ⚠ error now appears right next to the Save button, including whether a retry or an echo was involved, and the button label no longer wrongly says "Exporting…". ④ **Full config coverage**: seven fields that previously could only be edited by hand are now in the panel — the **master switch `enabled`**, single-vault `vaultDir`, distillation route `captureProvider` / `captureModel`, cooldown `captureCooldownMs`, semantic recall `recallEmbedding` and `sessionBudgetChars`; the host `/config` gained 6 keys and the standalone web `/config` gained 9. ⑤ New **permanent guard tests**: `tests/config-coverage.test.mjs` asserts every Config field is covered by the host API, the standalone web API and the settings panel (missing one turns the suite red), and `tests/settings-save.test.mjs` (5 checks) pins revision retry, overlay fallback and "never swallow errors". `npm test` now runs **121 checks** |
315
316
  | **v0.9.15** | 2026-09-29 | **Server-side graph speed-ups only — rendering is identical to v0.9.13.** ① **On-disk graph cache** keyed by fingerprint at `<vault>/memory-eternal-graph-cache.json` (atomic write, invalidated by fingerprint, skipped above 8MB, failures only cost a rebuild): measured cold start **422ms → 11ms** at 596 cards; ② **hashed bigrams + counting-sorted rare-gram index**: `bigramHashSet()` packs each bigram into a 32-bit integer (`Set<number>`; **capture de-duplication still uses the string version, so semantics are unchanged**), and the rare-gram index is built with a counting sort (202 → **54ms**); ③ **MinHash + LSH** candidate generation (48-dim signatures, 24 bands, unioned with the rare-gram index and then **verified exactly**): Jaccard calls 15,148 → 6,797 and the similarity stage 210 → 130ms. Full cold build **600 → 422ms**, edges unchanged from v0.9.13 (tag 2777 / similar 4). **Zero client changes** — no sprites, no static layer, no skeleton degradation (the v0.9.14 client work was reverted and deprecated because it hurt the feel). Adds `lib/minhash.js` + `tests/minhash.test.mjs` (4 checks); `npm test` runs **108 checks** |
316
317
  | **v0.9.13** | 2026-09-28 | **Seven fixes from user feedback.** ① **Version tracking**: Plugin info now shows **running (loaded by host) / on disk / npm latest** side by side, with an amber warning when disk is newer ("X is installed on disk but the running process still has Y — restart to apply") plus a "Check for updates" button (server-side npm lookup, 10-minute cache, 4s timeout). ② **Copy failure**: the memory dialog runs inside an iframe without clipboard-write permission, so `navigator.clipboard.writeText` is always rejected — it now tries the Clipboard API, falls back to `document.execCommand('copy')`, and finally shows the text in a read-only box with a Select-all button; the iframe also gets `allow="clipboard-write; clipboard-read"`. ③ **Silent save**: the result now appears right next to the Save button (⏳ Saving… / ✅ Saved / ⚠ error) and the button label changed from the wrong "Exporting…" to "Saving…". ④ **Recycle bin**: restore / purge / empty-all now report success and failure, and the empty state explains itself and disables the buttons. ⑤ **Button feedback everywhere**: the audit centre's approve / reject / delete used to swallow everything in `catch {}`; it now counts successes/failures and reports them (and warns when nothing is selected). ⑥ **Graph zoom jank**: new `src/client/graph-lod.js` — **viewport culling** (off-screen nodes/edges are skipped) + a **label budget** (≤120 nodes draw all; ≤300 draw 160; ≤600 draw 90; more draw 60, prioritising focus, hover, search hits and highest degree) + **text-width caching** + **shadows off for large graphs**. ⑦ Adds `tests/graph-lod.test.mjs` (5 checks); `npm test` now runs **108 checks** |
317
318
  | **v0.9.12** | 2026-09-28 | **New "Report a bug" entry (🐞 in the left rail)**: the dialog takes your description and then either ① **opens a pre-filled GitHub issue** (title, body, diagnostics and the `bug` label already filled — one more click on Submit files it), or ② **copies a ready-made AI prompt** (paste it into your chat and the AI runs `gh issue list` to de-duplicate, files the issue and reports the URL back). **Why not one-click submission?** GitHub forbids anonymous issue creation, a token shipped inside the plugin would be extracted by decompiling, and OAuth/relay needs hosted infrastructure — hence "pre-fill + one click". Diagnostics come from the new `/memory-eternal/api/diagnostics` endpoint and are **auto-redacted** (home directory → `~`; `sk-` / `ghp_` / `github_pat_` / `api_key:` → `***`; truncated to 2400 chars), and the issue URL is truncated by **binary search** so it can never exceed the length limit (`URLSearchParams` inflates CJK by roughly 9x). Adds `lib/feedback.js` (pure functions) and `tests/feedback.test.mjs` (4 checks: redaction, diagnostic fields and length cap, pre-filled URL encoding/truncation, bilingual prompt); `npm test` now runs **103 checks** |
package/README.md CHANGED
@@ -336,6 +336,7 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
336
336
 
337
337
  | 版本 | 日期 | 关键改动 |
338
338
  |---|---|---|
339
+ | **v0.9.16** | 2026-09-30 | **修复配置保存不生效(issue #12)+ 配置项全覆盖**。① **保存链路**:宿主 `dsh-settings.write()` 要求「面板持有的 revision 与 describe **完全一致**」,revision 一过期就抛 `SettingsConflictError` 导致保存失败 —— 现在遇到冲突会自动取最新 revision **重试一次**(提示里标 ♻)。② **宿主 volatile 回流滞后**:写成功后把改动叠加在本地视图(面板重开立刻是新值),等宿主快照追上再自动摘除;宿主 POST 新增**写入回读校验**,未回流时返回 `pending` 并提示「已写入,重启 DSH 后以配置文件为准」。③ **保存即时反馈**(你此前反馈过):保存按钮旁直接显示 ⏳ 保存中 / ✅ 已保存 / ⚠ 错误,并标明是否重试/是否回流;按钮文案修正(原来误显示「导出中…」)。④ **配置全覆盖**:补齐 7 个以前只能手改配置文件的字段 —— **总开关 `enabled`**、单库目录 `vaultDir`、蒸馏路由 `captureProvider` / `captureModel`、沉淀冷却 `captureCooldownMs`、语义召回 `recallEmbedding`、会话预算 `sessionBudgetChars`;宿主 `/config` 补 6 项、独立 web `/config` 补 9 项。⑤ 新增**永久守卫测试**:`tests/config-coverage.test.mjs` 保证每个 Config 字段都被「宿主 API / 独立 web API / 配置页」三处覆盖(漏一个就红),`tests/settings-save.test.mjs`(5 项)钉死 revision 重试、回流兜底与「错误不吞」。`npm test` 共 **121 项** |
339
340
  | **v0.9.15** | 2026-09-29 | **只带服务器侧图谱提速(渲染与 v0.9.13 完全一致)**。① **建图落盘缓存**:按指纹缓存 nodes+edges 到 `<vault>/memory-eternal-graph-cache.json`(原子写、指纹失效、超 8MB 不缓存、写失败只影响下次重建),596 卡实测冷启动 **422ms → 11ms**;② **bigram 哈希化 + 稀有 gram 计数排序**:`bigramHashSet()` 把 bigram 压成 32 位整数入 `Set<number>`(**写卡去重仍用字符串版,判定语义不变**),稀有倒排改按 df 计数排序(202 → **54ms**);③ **MinHash + LSH 候选生成**(48 维签名 / 24 band,与稀有倒排取并集后**精确校验**):jaccard 调用 15,148 → 6,797,相似度段 210 → 130ms。全量冷建图 **600 → 422ms**,边数与 v0.9.13 一致(tag 2777 / similar 4)。**客户端零改动**:不做精灵/静态层/骨架降级(v0.9.14 那三项因影响手感已回退并 deprecated)。新增 `lib/minhash.js` + `tests/minhash.test.mjs`(4 项),`npm test` 共 **108 项** |
340
341
  | **v0.9.13** | 2026-09-28 | **按用户反馈修 7 项**。① **版本跟踪**:配置页「插件信息」同时显示**运行中(宿主加载)/ 磁盘安装 / npm 最新**三个版本;磁盘比运行中新时给橙色警告(「磁盘已安装 X,但当前进程仍加载 Y —— 重启后才生效」),并新增「🔄 检查更新」(服务端查 npm,10 分钟缓存 + 4s 超时)。② **复制失败**:记忆弹窗在 iframe 里、默认没有 clipboard-write 权限,`navigator.clipboard.writeText` 必然被拒 —— 现在先试 Clipboard API,失败退 `document.execCommand('copy')`,再失败就把内容摊进只读文本框并给「全选」;iframe 加 `allow="clipboard-write; clipboard-read"`。③ **保存没有提示**:保存结果显示在保存按钮旁(⏳ 保存中… / ✅ 已保存 / ⚠ 错误),文案从错误的「导出中…」改为「保存中…」。④ **回收中心**:恢复 / 彻底删除 / 清空都有成功与失败提示,空库给说明与禁用态。⑤ **通用按键反馈**:审核中心的批准 / 驳回 / 删除原先 `catch{}` 吞掉一切,现逐条统计成功失败并提示;未选卡片也提示。⑥ **图谱缩放卡顿**:新增 `src/client/graph-lod.js` —— **视口剔除**(屏幕外节点/边不画)+ **标签预算**(≤120 全画 / ≤300 画 160 / ≤600 画 90 / 更多画 60,优先焦点、悬停、搜索命中、高度数)+ **文本宽度缓存** + **大图关阴影**。⑦ 新增 `tests/graph-lod.test.mjs`(5 项),`npm test` 共 **108 项** |
341
342
  | **v0.9.12** | 2026-09-28 | **新增「反馈异常」入口(左栏 🐞)**:弹窗里填完问题描述后可 ① **一键打开 GitHub 预填 issue**(标题 / 正文 / 诊断信息 / `bug` 标签都已填好,再点一次 Submit 即提交),或 ② **复制一段给 AI 的提示词**(贴进对话框,让 AI 先用 `gh issue list` 查重、再建 issue 并回报链接)。**为什么不做「点一下直接提交」**:GitHub 不允许匿名创建 issue;插件内置 token 会被反编译提取;OAuth / 中转需要自建服务 —— 所以走「预填 + 一次点击」。诊断信息由新接口 `/memory-eternal/api/diagnostics` 生成并**自动脱敏**(home 目录 → `~`,`sk-` / `ghp_` / `github_pat_` / `api_key:` 等 → `***`,截断到 2400 字符),issue 链接用**二分截断**保证不超长(`URLSearchParams` 会把中文膨胀约 9 倍)。新增 `lib/feedback.js`(纯函数层)与 `tests/feedback.test.mjs`(4 项:脱敏 / 诊断字段与限长 / 预填链接编码与截断 / 中英双语提示词),`npm test` 共 **103 项** |
package/index.js CHANGED
@@ -179,7 +179,7 @@ function schemaDefaults(schema) {
179
179
  return {}
180
180
  }
181
181
 
182
- function bindSettings(ctx, schema, config) {
182
+ export function bindSettings(ctx, schema, config) {
183
183
  const service = typeof ctx.get === 'function' ? ctx.get('settings') : ctx.settings
184
184
  if (service && typeof service.register === 'function') {
185
185
  return service.register('memory-eternal', schema, { base: config ?? {} })
@@ -194,16 +194,57 @@ function bindSettings(ctx, schema, config) {
194
194
  try { return service.configure({ auto: false }, ctx.fiber) } catch { return () => {} }
195
195
  }, 'memory-eternal: settings presentation')
196
196
  }
197
+ // 宿主「volatile 回流」可能滞后甚至不回流(issue #12:改了保存不上)。两步兜底:
198
+ // ① 写成功后把 patch 叠加在本地视图上 → 面板重新加载立刻是新值;
199
+ // ② 等宿主快照追上(值相等)后自动摘除,避免长期掩盖宿主的真实状态。
200
+ const overlay = {}
201
+ const snapshotWithoutOverlay = () => (config === undefined || config === null ? fallback : plainConfig(config))
202
+ const pruneOverlay = (snapshot) => {
203
+ for (const key of Object.keys(overlay)) {
204
+ if (snapshot && snapshot[key] !== undefined && JSON.stringify(snapshot[key]) === JSON.stringify(overlay[key])) delete overlay[key]
205
+ }
206
+ }
207
+ const readMerged = () => {
208
+ const base = snapshotWithoutOverlay()
209
+ pruneOverlay(base)
210
+ return Object.keys(overlay).length ? { ...base, ...overlay } : base
211
+ }
197
212
  return {
198
- get: read,
213
+ get: readMerged,
199
214
  watch(listener) {
200
- const handler = () => { try { listener(read()) } catch { /* 监听器异常不影响宿主 */ } }
215
+ const handler = () => { try { listener(readMerged()) } catch { /* 监听器异常不影响宿主 */ } }
201
216
  ctx.on('loader/volatile-update', handler)
202
217
  return () => { try { ctx.off('loader/volatile-update', handler) } catch { /* 已卸载 */ } }
203
218
  },
219
+ /**
220
+ * 写配置。
221
+ * 宿主 write() 用「describe 返回的 revision 必须完全一致」做乐观并发(dsh-settings
222
+ * SettingsConflictError),而面板持有的 revision 可能已经过期(上一次写入、宿主重建配置等)。
223
+ * 这里遇到冲突就取一次最新 revision 重试一次,而不是把 409 直接抛给用户。
224
+ * @returns {Promise<{revision:number, retried:boolean}>}
225
+ */
204
226
  async update(patch, expectedRevision) {
205
227
  if (!service || typeof service.update !== 'function') throw new Error('当前 DSH 版本不支持写配置')
206
- await service.update(entryId, patch, expectedRevision)
228
+ const call = (rev) => service.update(entryId, patch, rev)
229
+ let retried = false
230
+ let used = expectedRevision
231
+ try {
232
+ await call(expectedRevision)
233
+ } catch (error) {
234
+ const msg = String((error && error.message) || error)
235
+ if (!/revision|changed since|conflict/i.test(msg)) throw error
236
+ let fresh
237
+ try {
238
+ const list = typeof service.describe === 'function' ? service.describe() : []
239
+ fresh = (list.find((d) => d && d.ns === entryId) || {}).revision
240
+ } catch { fresh = undefined }
241
+ if (fresh === undefined) throw error
242
+ used = fresh
243
+ retried = true
244
+ await call(fresh)
245
+ }
246
+ Object.assign(overlay, patch)
247
+ return { revision: used, retried }
207
248
  },
208
249
  }
209
250
  }
@@ -766,6 +807,13 @@ export function apply(ctx, config) {
766
807
  // 多库(#10):配置页可直接编辑
767
808
  vaultProfiles: Array.isArray(cfg.vaultProfiles) ? cfg.vaultProfiles : [],
768
809
  activeVault: cfg.activeVault || '',
810
+ // 配置覆盖补齐(用户要求「配置按钮全覆盖」):这些以前只能手改配置文件
811
+ enabled: cfg.enabled !== false,
812
+ vaultDir: cfg.vaultDir || '',
813
+ captureProvider: cfg.captureProvider || '',
814
+ captureModel: cfg.captureModel || '',
815
+ recallEmbedding: cfg.recallEmbedding || '',
816
+ sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
769
817
  }
770
818
  const descriptor = (ctx.get('settings') ?? {}).describe?.({ redactSecrets: true }) ?? []
771
819
  const me = descriptor.find((d) => d.ns === 'memory-eternal')
@@ -799,10 +847,21 @@ export function apply(ctx, config) {
799
847
  if (Object.keys(clean).length === 0) return json(res, 400, { ok: false, error: '无可写入字段' })
800
848
  if (typeof settings.update === 'function') {
801
849
  try {
802
- await settings.update(clean, expectedRevision)
850
+ const writeResult = await settings.update(clean, expectedRevision)
803
851
  // 把完整配置写入共享文件,让独立 web / MCP hook 与 DSH 设置同步(不同步修复)
804
852
  syncConfigFile()
805
- return json(res, 200, { ok: true, applied: Object.keys(clean), note: '已保存。autoWebMode/watchdogAutoSpawn 等需重启 DSH 生效' })
853
+ // 回读校验:宿主 volatile 回流可能滞后(issue #12),此时明确告知而不是假装成功
854
+ const now = settings.get() ?? {}
855
+ const pending = Object.keys(clean).filter((k) => JSON.stringify(now[k]) !== JSON.stringify(clean[k]))
856
+ return json(res, 200, {
857
+ ok: true,
858
+ applied: Object.keys(clean),
859
+ pending,
860
+ retried: !!(writeResult && writeResult.retried),
861
+ note: pending.length
862
+ ? '已写入;宿主尚未回流这些值(面板已本地生效,重启 DSH 后以配置文件为准)'
863
+ : '已保存。autoWebMode/watchdogAutoSpawn 等需重启 DSH 生效',
864
+ })
806
865
  } catch (e) {
807
866
  if (e && e.code === 'SETTINGS_CONFLICT') return json(res, 409, { ok: false, error: '配置已被外部修改,请刷新后重试(revision conflict)' })
808
867
  return json(res, 500, { ok: false, error: String(e?.message || e) })
package/lib/api.js CHANGED
@@ -376,6 +376,9 @@ export function createApi(deps = {}) {
376
376
  autoWeb: cfg.autoWeb ?? env.MEMORY_AUTO_WEB !== '0', autoWebMode: cfg.autoWebMode || 'init', webPort: Number(cfg.webPort) || Number(env.MEMORY_WEB_PORT) || 7999, webCheckIntervalMs: Number(cfg.webCheckIntervalMs) || 5000, webMaxRestart: Number(cfg.webMaxRestart) || 10, watchdogAutoSpawn: cfg.watchdogAutoSpawn ?? env.MEMORY_WATCHDOG === '1', autoMcpSetup: cfg.autoMcpSetup ?? false,
377
377
  auditMode: cfg.auditMode ?? 'all', auditExemptAgents: cfg.auditExemptAgents ?? [], auditExemptKinds: cfg.auditExemptKinds ?? [], recycleRetentionDays: cfg.recycleRetentionDays ?? 30,
378
378
  vaultProfiles: Array.isArray(cfg.vaultProfiles) ? cfg.vaultProfiles : [], activeVault: cfg.activeVault || '',
379
+ enabled: cfg.enabled !== false, vaultDir: cfg.vaultDir || '', captureProvider: cfg.captureProvider || '',
380
+ captureModel: cfg.captureModel || '', recallEmbedding: cfg.recallEmbedding || '', sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
381
+ webPort: cfg.webPort ?? 7999, webCheckIntervalMs: cfg.webCheckIntervalMs ?? 5000, webMaxRestart: cfg.webMaxRestart ?? 10,
379
382
  }
380
383
  json(res, 200, { ok: true, config: safe, revision: 0, writable: false, readonly: true, version: env.MEMORY_ETERNAL_VERSION || '' })
381
384
  return