memory-eternal 0.10.9 → 0.10.11

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.10.9",
4
+ "version": "0.10.11",
5
5
  "description": "记忆核心(Memory 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.10.9",
3
+ "version": "0.10.11",
4
4
  "description": "记忆核心(Memory 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.10.9",
3
+ "version": "0.10.11",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.en.md CHANGED
@@ -361,14 +361,16 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
361
361
 
362
362
  | Version | Date | Highlights |
363
363
  |---|---|---|
364
- | **v0.10.9** | 2026-10-09 | **Root fix for the upgrade-window port drift that turned the memory page into "connection refused" (issue #29)**. The scene: a detached instance left by the old package sat on 7999 without reporting a version → the host `ensureWebServer()` could not recognise it, drifted to 8000 and wrote that into `web-info`; the plugin's self-heal then reaped the instance on 8000 as a stray → `web-info` dangled on a dead port (the page loaded once, then "connection refused 127.0.0.1" after a refresh). ① **The library page now uses a host same-origin shell**: the host gains `/memory-eternal/ui/app` (same index.html as the config page `/ui/config`, where `<script src="app.js">` resolves to `/ui/app.js`), so the iframe address is **decoupled from whichever port the resident instance listens on** (an older host still falls back to `web-info`). ② **`/web-info` converges live**: before answering, it probes "configured port → recorded port", uses whichever is serving, and reports `alive:false` when neither is (no more advertising a dead address); it is also rewritten right after a successful self-heal restart and whenever the interval keeper sees the instance alive. ③ **No more blind port drift**: new `looksLikeOurWeb()` (lenient detection of "ours but not reporting a version": any `ok:true` from /overview, /web-info or /budget) gives self-heal a 6s takeover window before drifting, and logs it when it does drift. ④ **Backoff retry in the health check**: `inspectResident()` no longer concludes "old instance that does not report its version" from a single missed probe (that verdict triggers a needless replacement on every start), it retries with backoff first. Adds `tests/issue-fixes-5.test.mjs` (4 checks). `npm test` now runs **287 checks** |
365
- | **v0.10.8** | 2026-10-08 | **Saving on the standalone web page now takes effect on click: the pending apply moves from a plugin callback into a host HTTP request (design A, measurement-driven)**. The scene: a standalone-page save ended in a `dropped` patch after five retries, always with `HMR transactions cannot be nested` (0.10.7 only stopped it from masquerading as an auto-capture anomaly and filtered upgrade-window keys — the sync itself was not fixed). **Measured on the same host with the same `settings.update`**: called from a plugin **callback** (`setInterval` / `fs.watch`) → guard error and **nothing was committed at all** (a patch with only host-known keys and a genuinely different value failed the same way); called from a plugin **HTTP request handler** → **it really committed** (`recallSummaryLen` 130→132→130, `revision` 4→6). So the fix is not another API but another **calling context**: ① the host gains `POST /memory-eternal/api/drain-pending`, applying the pending patch inside a **request handler** (two gates: loopback origin plus a per-process token from the heartbeat file, so no other local page can write); ② the host publishes its web server's **listening port + token** in the heartbeat (`webServer.port` is a public getter), and a standalone save now **wakes the host** and reports the real result (new `savedApplied` toast); ③ a failed wake-up (old host without the route → 404, port mismatch, timeout) is **not** an error — it falls back to the 0.10.5 waiting path and the "change it under DSH Settings" hint. Adds `tests/host-drain.test.mjs` (6 checks: loopback, the three gates, the end-to-end wake-up, failure reporting, 404 fallback, source guards) plus `tests/e2e-host-drain.mjs` (2 checks: a **real spawned process** end-to-end "standalone save → wake the host → host applies → reports applied", and the fallback to "queued" when the heartbeat comes from an old host with no port/token). `npm test` now runs **283 checks** |
364
+ | **v0.10.11** | 2026-10-09 | **One more layer of tolerance in distiller-output parsing: unescaped quotes inside string values (`Expected ',' or '}' after property value`)**. The scene: `UNPARSEABLE_OUTPUT` dominates the `fail` log (103 of the last 600 entries); the freshest one carries the full picture — `JSON.parse 原始报错:Expected ',' or '}' after property value in JSON at position 1913`, with an excerpt whose body reads `运行 "npm test" 时…`: the model wrote a quote into the string as content, so the parser took it for the string terminator. That is a **different** class from the bare control characters fixed in #24 (a control character is illegal inside a string, a quote merely needs escaping). Fix: new `repairUnescapedQuotes()` — a `"` only ends the string when the next non-space character is `:`, `,`, `}`, `]` or end-of-text, otherwise it is escaped as content; it participates **only as the last candidate** and the result must still pass the card health check (usable title + body ≥20 chars) — better to fail than to write a bad card. Guarded by `tests/capture-json-repair.test.mjs` (4 checks: identity on valid JSON, the live unescaped-quote case, control chars + quotes combined, truncation still failing honestly). `npm test` now runs **293 checks** |
365
+ | **v0.10.10** | 2026-10-09 | **The review centre can finally show card bodies, and you can approve/reject in place (issue #27)**. The scene (thinrflbtlm): the review centre listed only "title / kind / source / severity / reason" and **never the body**; and since v0.10.0 pending cards live physically quarantined, while the library lists approved cards only — so "no review without manual confirmation" made auditing pointless. ① Every row gains a ▸/▾ toggle (clicking the title works too) that **expands the frontmatter + body in place**, rendered with the **same Markdown pipeline as the card reader** (`renderMd` + `splitFrontmatter`), not raw text; ② the body is fetched **on demand** (`GET /card?path=…&status=pending|rejected`; the server still only allows paths that really sit in the queue, unknown paths keep returning 403 `CARD_NOT_APPROVED`), so the list endpoint never ships a whole store of bodies; ③ the expanded block carries **✓ approve / ✕ reject** directly (the recycle-bin tab uses ✓ restore / 🗑 delete), while bulk selection and the toolbar stay as they were; ④ approving/rejecting collapses and refreshes automatically, with both language dictionaries updated. Adds `tests/issue-fixes-6.test.mjs` (2 checks: a pending body is readable and leaves the queue once approved, plus source guards for on-demand fetching and the deny code). `npm test` now runs **289 checks** |
366
366
 
367
367
  <details>
368
- <summary>Older release notes (v0.10.7 and earlier · 34 releases, click to expand)</summary>
368
+ <summary>Older release notes (v0.10.9 and earlier · 36 releases, click to expand)</summary>
369
369
 
370
370
  | Version | Date | Highlights |
371
371
  |---|---|---|
372
+ | **v0.10.9** | 2026-10-09 | **Root fix for the upgrade-window port drift that turned the memory page into "connection refused" (issue #29)**. The scene: a detached instance left by the old package sat on 7999 without reporting a version → the host `ensureWebServer()` could not recognise it, drifted to 8000 and wrote that into `web-info`; the plugin's self-heal then reaped the instance on 8000 as a stray → `web-info` dangled on a dead port (the page loaded once, then "connection refused 127.0.0.1" after a refresh). ① **The library page now uses a host same-origin shell**: the host gains `/memory-eternal/ui/app` (same index.html as the config page `/ui/config`, where `<script src="app.js">` resolves to `/ui/app.js`), so the iframe address is **decoupled from whichever port the resident instance listens on** (an older host still falls back to `web-info`). ② **`/web-info` converges live**: before answering, it probes "configured port → recorded port", uses whichever is serving, and reports `alive:false` when neither is (no more advertising a dead address); it is also rewritten right after a successful self-heal restart and whenever the interval keeper sees the instance alive. ③ **No more blind port drift**: new `looksLikeOurWeb()` (lenient detection of "ours but not reporting a version": any `ok:true` from /overview, /web-info or /budget) gives self-heal a 6s takeover window before drifting, and logs it when it does drift. ④ **Backoff retry in the health check**: `inspectResident()` no longer concludes "old instance that does not report its version" from a single missed probe (that verdict triggers a needless replacement on every start), it retries with backoff first. Adds `tests/issue-fixes-5.test.mjs` (4 checks). `npm test` now runs **287 checks** |
373
+ | **v0.10.8** | 2026-10-08 | **Saving on the standalone web page now takes effect on click: the pending apply moves from a plugin callback into a host HTTP request (design A, measurement-driven)**. The scene: a standalone-page save ended in a `dropped` patch after five retries, always with `HMR transactions cannot be nested` (0.10.7 only stopped it from masquerading as an auto-capture anomaly and filtered upgrade-window keys — the sync itself was not fixed). **Measured on the same host with the same `settings.update`**: called from a plugin **callback** (`setInterval` / `fs.watch`) → guard error and **nothing was committed at all** (a patch with only host-known keys and a genuinely different value failed the same way); called from a plugin **HTTP request handler** → **it really committed** (`recallSummaryLen` 130→132→130, `revision` 4→6). So the fix is not another API but another **calling context**: ① the host gains `POST /memory-eternal/api/drain-pending`, applying the pending patch inside a **request handler** (two gates: loopback origin plus a per-process token from the heartbeat file, so no other local page can write); ② the host publishes its web server's **listening port + token** in the heartbeat (`webServer.port` is a public getter), and a standalone save now **wakes the host** and reports the real result (new `savedApplied` toast); ③ a failed wake-up (old host without the route → 404, port mismatch, timeout) is **not** an error — it falls back to the 0.10.5 waiting path and the "change it under DSH Settings" hint. Adds `tests/host-drain.test.mjs` (6 checks: loopback, the three gates, the end-to-end wake-up, failure reporting, 404 fallback, source guards) plus `tests/e2e-host-drain.mjs` (2 checks: a **real spawned process** end-to-end "standalone save → wake the host → host applies → reports applied", and the fallback to "queued" when the heartbeat comes from an old host with no port/token). `npm test` now runs **283 checks** |
372
374
  | **v0.10.7** | 2026-10-08 | **"Still the old version after starting dsh" fixed at the root: version-drift self-heal + one-click resident restart (the positive answer to #19 / #23).** The scene: an npm upgrade only swaps files on disk, and the **resident web (port 7999 by default) outlives the DSH host** — the process on the port keeps running the code it loaded at startup, while activation policy was "there is already a live watchdog on this port → `delegated` (no spawn, no replacement)", so **no number of DSH restarts ever replaces it** (measured here: the host started at 10:19 on the new build and the same-origin settings panel showed 0.10.6, while the web on 7999 had started at 08:15 on 0.10.0 — `/config` and `/version-check` each reported a different version). ① **Startup self-heal (`autoRestartOnDrift`, default on)**: activation first runs `inspectResident()` — the version is always asked from **the port** (the lock's `pkgVersion` is only the watchdog's self-report, and an old instance wrote an empty string, so drift was invisible) — then `decideResidentAction()` decides: match → `delegate` (keeping #19's honest wording), drift → `restart`, own version unreadable → touch nothing, just replaced → debounce (a config change re-runs the effect). **"Updating the running program" is therefore defined as "restart that resident process on the new code", never as hot-swapping modules in memory**: ESM module cache plus closure state (settings / vault routing / SSE hub / `fs.watch` / the already-bound port) leaves no safe way to hot-swap, and a cache-busting re-import only produces a second module graph (double instances, double listeners, double writes). ② **One implementation for replacement**: new `restartResident()` spawns a detached `watchdog.js --replace` helper (reap the old watchdog / old web → **wait for the port to really free up** → take the lock → pull up the new web), then verifies "the version served on the port equals the local version" (#23 suggestion 2); if the port never frees up it **aborts the takeover** instead of falling back to `port+1` (fallback instances are exactly where orphan webs are born). `dsh-memory restart`, host startup self-heal and the panel button now share it. The helper is detached with `stdio:ignore` (nobody reads its stderr), so the takeover outcome **must** go into the capture log — otherwise "restarted but not in effect" becomes yet another untraceable silent failure. ③ **One-click fix in the panel**: new `POST /memory-eternal/api/restart-self` under three hard rules — this process may well **be** the stale web being replaced, so the response is sent **before** the replacement is scheduled; if the resident instance is already current it answers `alreadyCurrent` (the common case when clicking from the DSH panel — a healthy instance is never needlessly taken down); and only one replacement may run at a time. ④ **Panel semantics disambiguated**: "running" became "**running (process serving this page)**" (the resident web on the standalone page, the host inside DSH Settings — the very reason the two panels' numbers disagreed), plus a new "**resident instance vX**" chip so it is obvious which one to restart; the drift banner offers the button plus the equivalent command, and when the resident instance is already new but this page is not, it says plainly "restart the desktop app / DSH" instead of offering a button that would do nothing. ⑤ **`updateAvailable` now compares versions**: `latest !== onDisk` used to **advise downgrading** when running an unpublished local build (disk 0.10.7 / npm 0.10.6); it now uses `compareVersions` (with prerelease semantics, `rc.2 < rc.10`). ⑥ Also removed a duplicate `saving` key from the client dictionary (esbuild warned on every build). New `tests/watchdog-restart.test.mjs` (17 checks: decision matrix / inspection asking the port / replacement verification / non-lethal takeover cleanup / route responding before replacing + concurrency gate / version comparison). ⑦ **The README now ships as two separate files**: `README.md` is the **default** Simplified Chinese one (what npm and the DSH marketplace render) and `README.en.md` is the English mirror; both gained a language switch plus a "change one, change the other" note at the top, and the English self-diagnostics subsection that was missing; new `tests/readme-i18n.test.mjs` locks **1:1 structure** (heading-level sequence + code-block count + cross links + default-language markers) so future edits cannot land on one side only. ⑧ **`secretHint` credential hint + credential-read guard**: one line of your own convention gets injected into every session's systemPrompt so the agent looks up the **secret directory card** in memory first when it needs an API key / token (memory stores the directory only — names / locations / how to use — while **values stay in local credential stores**); `dsh-memory setup` now also registers the Claude Code `PreToolUse` guard (denies reads of `.credentials.yaml` / `.env` / `*-token` and points at the loader) and refreshes hook paths that still point at an older install. ⑨ **Three more live-site findings fixed before release**: ① the panel's **restart resident instance** button also showed when the *process serving this page* was the old one — clicking it must 404 (the old host has no such route) and the user only saw "unknown interface"; it now appears only when the resident instance on the port is behind the files on disk, and a 404/405 gets explicit "restart the desktop build / DSH, or run dsh-memory restart" guidance. ② When the standalone page (new) submits the whole form to an old host, the host drops the keys it does not know (e.g. `secretHint`) by whitelist → the read-back check can never match → five retries then `dropped`, with the user only seeing "HMR transactions cannot be nested"; the pending patch is now **filtered against the host schema** before it is applied (unknown keys take effect once the host upgrades), and "config sync failed" is kept strictly apart from "auto-capture anomaly" (`config-warn` / `config-fail`, no longer painting the capture health red). ③ `autoRestartOnDrift` now renders as **on** when the host snapshot has no such key (measured: 0.10.6 host + 0.10.7 panel → checkbox empty while the host actually treats it as on). Adds `tests/issue-fixes-4.test.mjs`. `npm test` now runs **274 checks** |
373
375
  | **v0.10.6** | 2026-10-07 | **Orphan webs are now reclaimed + a failed shared-config sync is no longer silent.** ① **Orphan webs**: the web child is spawned detached, so when a watchdog is hard-killed (on Windows `process.kill(pid,'SIGTERM')` terminates unconditionally) its exit handler never runs; if that web had **fallen back to port+1…** because the target port was taken, the lock entry naming it disappears together with the watchdog — leaving an unowned web parked on 8001/8002 (two were found on this machine, one of them 20 hours old, and `status` only probes the configured port so it could never see them). Now `startWatchdog` on startup, and `stop`/`restart` (the `checkPort` path), sweep the **fallback window** `[port+1, port+9]` and reap them; three gates must all pass (the port is inside the window / the command line **really is this plugin's web** / the pid is not any watchdog's `pid` or `webPid` in the lock), so **someone else's `node web.js` is never killed**. In addition `spawnWeb` no longer spawns a duplicate when the target port already holds our own web (the main source of fallback instances) — liveness is left to the tick probe. ② **`syncConfigFile()` no longer swallows errors**: it writes the read source shared by the standalone page / hooks / MCP, so a failed write shows up as "config changed but they can't see it" — the old code was `catch { /* silent */ }` (observed once: the shared file's mtime stayed at pre-activation while the host's own value had changed). Failures now reach stderr and the capture log as a `warn` (same reason reported once, no spam); because `logCapture` is defined later inside `apply()`, the log line is deliberately deferred by one tick to avoid the `const` TDZ. New `tests/watchdog-orphans.test.mjs` (7 checks); `npm test` now runs **245 checks** |
374
376
  | **v0.10.5** | 2026-10-07 | **Config saves from the standalone web page: no more empty promise, and no DSH install required.** ① **A host-independent write path (#21)**: if nothing consumes the pending file within 800 ms **and** no live DSH host is present (the heartbeat file `memory-eternal-config.host.json` is stale, or its pid is dead), the standalone side now **atomically merges** the change straight into the shared config, clears the pending file and answers `pendingOutcome: applied-direct` — so Codex / Claude Code / Cursor users, and anyone running only `dsh-memory serve`, get their save applied immediately (previously that path just wrote a "pending" file for a host that **does not exist**, while the UI promised "takes effect on next start" forever). When a host *is* alive the standalone side **never** writes directly: the shared file is the host's derived mirror, and a direct write would be overwritten by the next `syncConfigFile()` (a dedicated reverse test guards that boundary). ② **Failures now give directions, not just an error (#21)**: host-guard errors are recognised — `HMR transactions cannot be nested` (Cordis HMR hosts) and `root.events.emit is unavailable from a plugin activation` (dsh-tui and friends) are two phrasings of one defect family — and "this host does not let a plugin write config from its own callback context" plus where to change it instead (**DSH Settings → Memory**, or the profile patch layer) now lands in the give-up error, the diagnostics and the standalone page's hint; `POST /config` also returns `hostGuard` / `hint`. ③ **The UI no longer always says "Saved" (#21)**: the client consumes `pendingOutcome` — `queued` / `failed` use the non-success style and say "not confirmed applied yet", `applied-direct` states "no DSH host here, written straight into the shared config", and only a real apply shows "Saved". ④ Also fixed a cleanup race that could keep a host or test process **alive forever**: the heartbeat timer and the pending-file watcher are created after an async `import()`, so a dispose that happened before it resolved left them uncleaned (the heartbeat interval is additionally `unref()`ed) — this is exactly what hung `tests/settings-compat.test.mjs` when it ran `apply()` against a fake ctx. New `tests/issue-fixes-3.test.mjs` (9 checks); `npm test` now runs **238 checks** |
package/README.md CHANGED
@@ -393,14 +393,16 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
393
393
 
394
394
  | 版本 | 日期 | 关键改动 |
395
395
  |---|---|---|
396
- | **v0.10.9** | 2026-10-09 | **升级期端口漂移把记忆页打成「拒绝连接」的根治(issue #29)**。现场:旧包遗留的 detached 实例占着 7999 且不自报版本 → 宿主 `ensureWebServer()` 认不出它、向后漂到 8000 并写进 `web-info`;而插件的自愈随后把 8000 上的实例当野实例收掉 → `web-info` 永久悬空,客户端 iframe 指向死端口(首次能进、刷新即「拒绝连接 127.0.0.1」)。① **记忆库页改走 host 同源壳**:宿主新增 `/memory-eternal/ui/app`(与配置页 `/ui/config` 同一份 index.html,其中 `<script src="app.js">` 相对路径正好解析到 `/ui/app.js`),iframe 地址从此与「常驻实例监听哪个端口」**彻底解耦**(旧宿主 404 时仍回落 `web-info`)。② **`/web-info` 实时收敛**:每次读之前先探「配置端口 → 记录端口」,谁在服务就用谁,都不通就如实 `alive:false`(不再把死地址当活的);自愈替换成功后与 interval 保活探到活时都就地回写。③ **不再盲目漂端口**:新增 `looksLikeOurWeb()`(宽松识别「自家但不自报版本」的占用者:/overview、/web-info、/budget 任一 ok:true 即认),疑似自家时先给自愈留 6s 接管窗口,等不到才漂并留日志。④ **体检退避重试**:`inspectResident()` 一次探针打空不再直接判「旧版不自报版本」(那个结论会触发误替换,每次启动都白起一次实例),退避重试后才下结论。新增 `tests/issue-fixes-5.test.mjs`(4 项:宽松识别真/假阳性、等自愈接管不漂端口、宿主与客户端源码守卫)。`npm test` 共 **287 项** |
397
- | **v0.10.8** | 2026-10-08 | **独立 Web 端保存「点一下即生效」:把应用 pending 的上下文从插件回调搬到宿主 HTTP 请求(方案 A,实测驱动)**。现场:独立页保存 → 宿主连续重试 5 次后 `dropped`,报错恒为 `HMR transactions cannot be nested`(0.10.7 只修掉了它冒充「自动沉淀异常」与升级期未知键,同步本身没修)。**实测定位**(同一宿主、同一个 `settings.update`):从插件**回调**(`setInterval` / `fs.watch`)里调 → 抛守卫错且**一个字节都没提交**(只含宿主认识的键、值真的变了,同样失败);从插件 **HTTP 请求处理器**里调 → **真的提交**(`recallSummaryLen` 130→132→130,`revision` 4→6)。所以修法不是换 API 而是换**调用上下文**:① 宿主新增 `POST /memory-eternal/api/drain-pending`,在**请求处理器**里应用 pending(两道闸门:只接受本机来源 + 心跳里的一次性令牌,防本机任意页面乱写);② 宿主把自己 web server 的**监听端口 + 令牌**写进心跳(`webServer.port` 是服务公开的 getter),独立端保存后据此**主动叫醒宿主**,拿到真实结果再回给界面(新增 `savedApplied`「已保存并生效」);③ 叫醒失败(旧宿主没这条路由 404 / 端口对不上 / 超时)**不算错误** —— 回落到 0.10.5 的等待路径与「去 DSH 设置里改」的指路。新增 `tests/host-drain.test.mjs`(6 项:回环判定 / 守卫三件套 / 端到端叫醒 / 失败如实回报 / 404 回落 / 源码守卫)+ `tests/e2e-host-drain.mjs`(2 项:**真起进程**的端到端「独立页保存 → 叫醒宿主 → 宿主应用 → 回报已生效」,以及旧宿主心跳(无端口/令牌)时回落成「排队等待」)。`npm test` 共 **283 项** |
396
+ | **v0.10.11** | 2026-10-09 | **蒸馏输出解析再补一层容错:字符串里的裸引号(`Expected ',' or '}' after property value`)**。现场:`fail` 日志里 `UNPARSEABLE_OUTPUT` 占大头(最近 600 条里 103 条),最近一条带全了现场 —— `JSON.parse 原始报错:Expected ',' or '}' after property value in JSON at position 1913`,摘录里能看到正文写着 `运行 "npm test" 时…`:模型把引号当正文直接写进字符串,于是它被当成字符串结束符。这与 #24 修过的「裸控制字符」是**不同**的一类(控制字符在字符串内非法,引号只是需要转义)。修法:新增 `repairUnescapedQuotes()` —— 一个 `"` 后面(跳过空白)紧跟 `:` `,` `}` `]` 或文本结尾才算字符串结束,否则当正文补 `\"`;它**只作为最后一个候选**参与解析,且必须仍通过卡片体检(标题合格 + 正文 ≥20 字)才被接受 —— 宁可不修,也不写坏卡。回归守卫 `tests/capture-json-repair.test.mjs`(4 项:合法 JSON 恒等变换、裸引号现场、控制字符与裸引号叠加、截断仍如实失败)。`npm test` 共 **293 项** |
397
+ | **v0.10.10** | 2026-10-09 | **审核中心能看正文了 + 就地批准/驳回(issue #27)**。现场(thinrflbtlm):审核中心过去只列「标题 / 类型 / 来源 / 严重度 / 原因」,**看不到正文**;而 v0.10.0 之后待审卡物理隔离在 `quarantine`、知识卡界面只列已审核卡 —— 于是「不人工确认就没法审」,审核形同虚设。① 每行新增 ▸/▾(点标题也行)**就地展开 frontmatter + 正文**,用与卡片详情**同一套 Markdown 渲染**(`renderMd` + `splitFrontmatter`),不是裸文本;② 正文**按需拉取**(`GET /card?path=…&status=pending|rejected`,服务端只放行确实在审核队列里的 path,未知 path 仍然 403 `CARD_NOT_APPROVED`),列表接口不塞整库正文;③ 展开块里直接给 **✓ 批准 / ✕ 驳回**(回收站页签是 ✓ 恢复 / 🗑 删除),批量勾选与外层工具条保持原样;④ 批/驳后自动收起并刷新,中英双语词条齐全。新增 `tests/issue-fixes-6.test.mjs`(2 项:待审正文读得到且批准后离开队列、源码守卫含「按需拉取」与「越权拒绝码」)。`npm test` 共 **289 项** |
398
398
 
399
399
  <details>
400
- <summary>更早的更新日志(v0.10.7 及以前 · 共 34 版,点开查看)</summary>
400
+ <summary>更早的更新日志(v0.10.9 及以前 · 共 36 版,点开查看)</summary>
401
401
 
402
402
  | 版本 | 日期 | 关键改动 |
403
403
  |---|---|---|
404
+ | **v0.10.9** | 2026-10-09 | **升级期端口漂移把记忆页打成「拒绝连接」的根治(issue #29)**。现场:旧包遗留的 detached 实例占着 7999 且不自报版本 → 宿主 `ensureWebServer()` 认不出它、向后漂到 8000 并写进 `web-info`;而插件的自愈随后把 8000 上的实例当野实例收掉 → `web-info` 永久悬空,客户端 iframe 指向死端口(首次能进、刷新即「拒绝连接 127.0.0.1」)。① **记忆库页改走 host 同源壳**:宿主新增 `/memory-eternal/ui/app`(与配置页 `/ui/config` 同一份 index.html,其中 `<script src="app.js">` 相对路径正好解析到 `/ui/app.js`),iframe 地址从此与「常驻实例监听哪个端口」**彻底解耦**(旧宿主 404 时仍回落 `web-info`)。② **`/web-info` 实时收敛**:每次读之前先探「配置端口 → 记录端口」,谁在服务就用谁,都不通就如实 `alive:false`(不再把死地址当活的);自愈替换成功后与 interval 保活探到活时都就地回写。③ **不再盲目漂端口**:新增 `looksLikeOurWeb()`(宽松识别「自家但不自报版本」的占用者:/overview、/web-info、/budget 任一 ok:true 即认),疑似自家时先给自愈留 6s 接管窗口,等不到才漂并留日志。④ **体检退避重试**:`inspectResident()` 一次探针打空不再直接判「旧版不自报版本」(那个结论会触发误替换,每次启动都白起一次实例),退避重试后才下结论。新增 `tests/issue-fixes-5.test.mjs`(4 项:宽松识别真/假阳性、等自愈接管不漂端口、宿主与客户端源码守卫)。`npm test` 共 **287 项** |
405
+ | **v0.10.8** | 2026-10-08 | **独立 Web 端保存「点一下即生效」:把应用 pending 的上下文从插件回调搬到宿主 HTTP 请求(方案 A,实测驱动)**。现场:独立页保存 → 宿主连续重试 5 次后 `dropped`,报错恒为 `HMR transactions cannot be nested`(0.10.7 只修掉了它冒充「自动沉淀异常」与升级期未知键,同步本身没修)。**实测定位**(同一宿主、同一个 `settings.update`):从插件**回调**(`setInterval` / `fs.watch`)里调 → 抛守卫错且**一个字节都没提交**(只含宿主认识的键、值真的变了,同样失败);从插件 **HTTP 请求处理器**里调 → **真的提交**(`recallSummaryLen` 130→132→130,`revision` 4→6)。所以修法不是换 API 而是换**调用上下文**:① 宿主新增 `POST /memory-eternal/api/drain-pending`,在**请求处理器**里应用 pending(两道闸门:只接受本机来源 + 心跳里的一次性令牌,防本机任意页面乱写);② 宿主把自己 web server 的**监听端口 + 令牌**写进心跳(`webServer.port` 是服务公开的 getter),独立端保存后据此**主动叫醒宿主**,拿到真实结果再回给界面(新增 `savedApplied`「已保存并生效」);③ 叫醒失败(旧宿主没这条路由 404 / 端口对不上 / 超时)**不算错误** —— 回落到 0.10.5 的等待路径与「去 DSH 设置里改」的指路。新增 `tests/host-drain.test.mjs`(6 项:回环判定 / 守卫三件套 / 端到端叫醒 / 失败如实回报 / 404 回落 / 源码守卫)+ `tests/e2e-host-drain.mjs`(2 项:**真起进程**的端到端「独立页保存 → 叫醒宿主 → 宿主应用 → 回报已生效」,以及旧宿主心跳(无端口/令牌)时回落成「排队等待」)。`npm test` 共 **283 项** |
404
406
  | **v0.10.7** | 2026-10-08 | **「启动 dsh 后运行中还是旧版本」根治:版本漂移自愈 + 一键重启常驻实例(#19 / #23 的正面解法)**。现场:升级 npm 包只换了磁盘文件,**常驻 web(默认 7999)比 DSH 宿主活得久** —— 端口上那个进程仍在跑「启动时加载进内存」的旧代码;而宿主激活时的策略是「同端口已有活着的 watchdog 就 `delegated`(不再 spawn、更不替换)」,于是**重启多少次 DSH 都不会换掉它**(实测本机:宿主 10:19 启动已是新版、设置页同源显示 0.10.6,而 7999 上的 web 是 08:15 启动的 0.10.0,`/config` 与 `/version-check` 各报一个版本)。① **启动自愈(`autoRestartOnDrift`,默认开)**:激活时先 `inspectResident()` 体检 —— 版本一律**问端口**(锁里的 `pkgVersion` 只是 watchdog 自述,旧实例写的还是空串,根本看不出版本漂移),再 `decideResidentAction()` 决策:一致 → `delegate`(保持 #19 的如实口径)、漂移 → `restart`、读不到本机版本 → 绝不乱动、刚替换过 → 防抖不重来(配置改动会让 effect 重跑)。**「更新运行中的程序」的正确形态由此定为「用新代码重启那个常驻进程」,而不是热替换内存里的模块**:ESM 模块缓存 + 闭包状态(settings / vault 路由 / SSE hub / `fs.watch` / 已监听端口)没有安全热换的余地,缓存失效重 import 只会得到第二份模块图(双实例、双监听、双写)。② **替换机制统一到一处**:新增 `restartResident()` = spawn 一个 detached 的 `watchdog.js --replace` 助手(收旧 watchdog / 旧 web → **等端口真的空出来** → 抢锁 → 拉起新 web),再自检「端口上真正服务的版本 = 本机版本」(#23 建议 2);端口没空出来就**中止接管**、不退让到 `port+1`(退让实例正是孤儿 web 的诞生地)。`dsh-memory restart`、宿主启动自愈、面板按钮现在共用这一份实现。助手是 detached + `stdio:ignore`(stderr 没人看),所以接管结论**必须**写进「自动沉淀日志」—— 否则「重启了却没生效」又会变成一条无迹可查的静默失败。③ **面板一键修复**:新增 `POST /memory-eternal/api/restart-self`,三条硬约束 —— 本进程很可能**就是**要被替换的旧 web,所以**先回响应再调度**替换;常驻实例已经是最新就回 `alreadyCurrent`(宿主面板点这个按钮时最常见,绝不为一个健康实例白折腾一次停机);同一时刻只允许一个替换在跑。④ **面板语义消歧**:「运行中」改为「**运行中(服务本页的进程)**」(独立 Web 页里它是常驻 web,DSH 设置页里是宿主 —— 这正是两个面板数字「打架」的根源),并新增「**常驻实例 vX**」徽标:两者不同时一眼看出该重启谁;漂移横幅给按钮 + 等价命令,若常驻实例已新而本页仍旧,则明说「需重启桌面版 / DSH」,不再给一个点了也没用的按钮。⑤ **`updateAvailable` 改按版本比较**:`latest !== onDisk` 在本地跑未发布版本时会**反过来劝你装回旧版**(磁盘 0.10.7 / npm 0.10.6),现在用 `compareVersions`(含预发布语义 `rc.2 < rc.10`)。⑥ 顺带清掉客户端词典里重复的 `saving` 键(esbuild 每次构建都在报警告)。新增 `tests/watchdog-restart.test.mjs`(17 项:决策矩阵 / 体检问端口 / 替换自检 / 接管前清场不误杀 / 路由先响应后替换 + 并发闸门 / 版本比较)。⑦ **README 改为中英两份独立文件**:`README.md` 是**默认**的简体中文(npm 与 DSH 插件市场展示这一份),`README.en.md` 是英文镜像;顶部加语言切换与「改一处要同步另一处」的说明,并补齐英文版缺失的「环境自检」小节;新增 `tests/readme-i18n.test.mjs` 锁住**结构 1:1**(标题层级序列 + 代码块数量 + 互链 + 默认语言标记),防止以后只改一份。⑧ **`secretHint` 凭证提示 + 凭据读取守卫**:填一句自己的约定就会被注入每个会话的 systemPrompt,让 agent 需要 API key / token 时**先查记忆里的「密钥目录」卡**(记忆库只存目录:名字/位置/取用方式,**值留在本机凭据库**);`dsh-memory setup` 同时注册 Claude Code 的 `PreToolUse` 守卫(拒读 `.credentials.yaml` / `.env` / `*-token` 并指路到加载器),并会刷新仍指向旧安装副本的 hook 路径。⑨ **发布前又抓到三个实测现场并修掉**:① 面板「重启常驻实例」按钮在**服务本页的进程**是旧版时也显示 —— 点了必然 404(旧宿主没有这条路由),用户只看到一句「未知接口」;现在按钮只在「端口上那个常驻实例落后于磁盘」时出现,404/405 还会给一条「重启桌面版 / DSH 或执行 dsh-memory restart」的指路。② 独立页(新版)提交整张表单、宿主还是旧版时:宿主按白名单丢掉它不认识的键(如 `secretHint`)→ 回读校验必然对不上 → 连试 5 次 `dropped`,用户只看到「HMR transactions cannot be nested」;现在应用前按宿主 schema **过滤未知键**(宿主升级后自然生效),并把「配置同步失败」与「自动沉淀异常」彻底分开(`config-warn` / `config-fail`,不再翻红沉淀健康状态)。③ `autoRestartOnDrift` 在宿主快照里没有这个键时按**默认开**渲染(实测现场:0.10.6 宿主 + 0.10.7 面板 → 复选框空着,而宿主实际按开处理,面板与真实行为不一致)。新增 `tests/issue-fixes-4.test.mjs`。`npm test` 共 **274 项** |
405
407
  | **v0.10.6** | 2026-10-07 | **孤儿 web 有了回收机制 + 共享配置同步失败不再静默**。① **孤儿 web**:web 子进程是 detached 启动的,watchdog 被硬终止时(Windows 上 `process.kill(pid,'SIGTERM')` 即无条件终止)它的退出处理器不执行;若这个 web 当初因为目标端口被占而**退让到 port+1…**,登记它的锁条目又随 watchdog 一起消失 —— 于是 8001/8002 这类端口上会常驻没人认领的 web(实测本机就躺着两个,其中一个已跑了 20 小时,而 `status` 只探配置端口,永远发现不了)。现在 `startWatchdog` 启动时、以及 `stop`/`restart`(`checkPort` 路径)都会扫**退让窗口** `[port+1, port+9]` 并收掉它们;三道门缺一不可(端口在窗口内 / 命令行**确实是本插件的 web** / pid 不在锁里任何 watchdog 的 `pid` 或 `webPid` 上),**别人的 `node web.js` 一个都不杀**。另外 `spawnWeb` 发现目标端口上已有自家 web 时**不再重复拉起**(退让实例的主要来源),只交给 tick 探活。② **`syncConfigFile()` 不再静默吞错**:它写的是独立页 / hooks / MCP 共用的读源,写不进去就表现为「配置改了但他们看不到」——旧实现是 `catch { /* 静默 */ }`(实测到过一次:共享文件 mtime 停在激活前,而宿主里的值已经变了)。现在失败会进 stderr 与「自动沉淀日志」的 warn(同一原因只报一次,不刷屏);因为 `logCapture` 定义在 `apply()` 更靠后的位置,日志刻意**延迟一拍**再写,避开 `const` 的 TDZ。新增 `tests/watchdog-orphans.test.mjs`(7 项),`npm test` 共 **245 项** |
406
408
  | **v0.10.5** | 2026-10-07 | **独立 Web 端的配置保存:不再空头承诺,也不再要求「必须装了 DSH」**。① **宿主无关的直写路径(#21)**:独立端保存后若 800ms 内没人消费 pending 文件、**且**本机没有活着的 DSH 宿主(心跳文件 `memory-eternal-config.host.json` 不新鲜、或里面的 pid 已死),就直接把改动**原子合并**写进共享配置、清掉 pending,响应回 `pendingOutcome: applied-direct` —— 于是 Codex / Claude Code / Cursor、以及只跑 `dsh-memory serve` 的用户,保存当场生效(此前那条路只会写「待应用」等一个**根本不存在**的宿主,界面上那句「下次启动生效」永远不会兑现)。宿主活着时**绝不**直写:共享配置是宿主的派生镜像,绕过它会被下一次 `syncConfigFile()` 盖回去(有专门的反向测试守着这条边界)。② **失败要指路,不只是重复报错(#21)**:识别「宿主守卫类」报错 —— `HMR transactions cannot be nested`(Cordis HMR 宿主)与 `root.events.emit is unavailable from a plugin activation`(dsh-tui 一类)是同一族缺陷的两种措辞 —— 并把「此宿主不允许插件从自己的回调上下文写配置」与「去哪改」(**DSH 设置 → 记忆**,或 profile 的配置补丁层)写进放弃时的报错、诊断信息与独立页提示;`POST /config` 同时回 `hostGuard` / `hint` 字段。③ **界面不再一律弹「已保存」(#21)**:客户端开始消费 `pendingOutcome` —— `queued` / `failed` 用**非成功样式**并说明「尚未确认生效」,`applied-direct` 明确写「本机没有 DSH 宿主,已直接写入共享配置」,只有真生效才显示「已保存」。④ 顺带修掉一个会让宿主/测试**无法退出**的清理竞态:心跳定时器与 pending 文件监听都是在异步 `import()` 之后才建立,若在它 resolve 之前就 dispose,回调就再没人清理(心跳定时器还额外 `unref()`)—— `tests/settings-compat.test.mjs` 用假 ctx 跑 `apply()` 时正是被它挂住的。新增 `tests/issue-fixes-3.test.mjs`(9 项),`npm test` 共 **238 项** |
package/lib/capture.js CHANGED
@@ -418,6 +418,44 @@ export function repairJsonControlChars(text) {
418
418
  return { text: out, repaired }
419
419
  }
420
420
 
421
+ /**
422
+ * 修复「字符串值里未转义的裸引号」(10/8 实测现场)。
423
+ *
424
+ * 现场:`JSON.parse 原始报错:Expected ',' or '}' after property value in JSON at position 1913`,
425
+ * 摘录里能看到正文写着 `运行 "npm test" 时…` —— 模型把引号当正文内容直接写进字符串,于是这个
426
+ * 引号被当成字符串结束符,后面的中文就变成「属性值后面还有东西」。这与已经修过的「裸控制字符」
427
+ * (#24)是**不同**的一类:控制字符在字符串内部是非法的,而引号在字符串内部是「合法但需要转义」。
428
+ *
429
+ * 启发式(只作为**最后一个候选**,见 parseCaptureJsonDetailed):一个 `"` 只有当它后面(跳过空白)
430
+ * 紧跟 `:` `,` `}` `]` 或文本结尾时才算「字符串结束」,否则补 `\"`。
431
+ * 因为可能误修,调用方必须再通过卡片体检(`cardFromParsedJson`)才接受 —— 宁可不修,也不写坏卡。
432
+ *
433
+ * @returns {{ text: string, repaired: boolean }} repaired=true 表示确实补过反斜杠
434
+ */
435
+ export function repairUnescapedQuotes(text) {
436
+ const s = String(text || '')
437
+ let out = ''
438
+ let inStr = false
439
+ let esc = false
440
+ let repaired = false
441
+ for (let i = 0; i < s.length; i++) {
442
+ const ch = s[i]
443
+ if (esc) { esc = false; out += ch; continue }
444
+ if (ch === '\\') { esc = true; out += ch; continue }
445
+ if (ch !== '"') { out += ch; continue }
446
+ if (!inStr) { inStr = true; out += ch; continue }
447
+ // 已在字符串里:看这个引号后面是否紧跟结构字符
448
+ let j = i + 1
449
+ while (j < s.length && /\s/.test(s[j])) j++
450
+ const next = j < s.length ? s[j] : ''
451
+ const endsString = next === '' || next === ':' || next === ',' || next === '}' || next === ']'
452
+ if (endsString) { inStr = false; out += ch; continue }
453
+ repaired = true
454
+ out += '\\"'
455
+ }
456
+ return { text: out, repaired }
457
+ }
458
+
421
459
  /**
422
460
  * 失败现场摘要:首尾各留一段,中间省略多少字写清楚。
423
461
  *
@@ -479,6 +517,12 @@ export function parseCaptureJsonDetailed(text) {
479
517
  const fixed = repairJsonControlChars(base)
480
518
  if (fixed.repaired) candidates.push({ text: fixed.text, repaired: true })
481
519
  }
520
+ // 最后一层:字符串内裸引号(10/8 现场)。它对合法 JSON 是恒等变换(repaired=false 时不入列),
521
+ // 且必须仍能通过卡片体检才会被接受。
522
+ for (const candidate of [...candidates]) {
523
+ const fixed = repairUnescapedQuotes(candidate.text)
524
+ if (fixed.repaired) candidates.push({ text: fixed.text, repaired: true })
525
+ }
482
526
  let lastError = ''
483
527
  for (const candidate of candidates) {
484
528
  let parsed