memory-eternal 0.10.6 → 0.10.8

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.6",
4
+ "version": "0.10.8",
5
5
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
6
6
  "author": {
7
7
  "name": "EternalNight996",
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
7
7
  "url": "https://github.com/EternalNight996"
8
8
  },
9
9
  "license": "MIT",
10
+ "keywords": [
11
+ "deepseek-harness",
12
+ "dsh-plugin",
13
+ "memory",
14
+ "knowledge-base",
15
+ "mcp",
16
+ "markdown",
17
+ "vault"
18
+ ],
10
19
  "homepage": "https://github.com/EternalNight996/dsh-memory-eternal",
11
20
  "repository": "https://github.com/EternalNight996/dsh-memory-eternal",
12
21
  "skills": "./skills/",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/.mcp.json CHANGED
@@ -1,9 +1,9 @@
1
- {
2
- "mcpServers": {
3
- "memory": {
4
- "command": "node",
5
- "args": ["${CLAUDE_PLUGIN_ROOT}/bin/dsh-memory.mjs", "mcp"],
6
- "env": {}
7
- }
8
- }
9
- }
1
+ {
2
+ "mcpServers": {
3
+ "memory": {
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/dsh-memory.mjs", "mcp"],
6
+ "env": {}
7
+ }
8
+ }
9
+ }
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 EternalNight996
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EternalNight996
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 🧠 memory-eternal — A "second brain" for your AI
2
2
 
3
+ [![HOL Guard](https://img.shields.io/endpoint?url=https%3A%2F%2Fhol.org%2Fapi%2Fregistry%2Fbadges%2Fplugin%3Fslug%3Deternalnight996%252Fmemory-eternal%26metric%3Dtrust)](https://hol.org/registry/plugins/eternalnight996%2Fmemory-eternal)
4
+
5
+ <!-- Language / 语言:this is the **English mirror**; the default README is Chinese (README.md).
6
+ The two are **separate files** with 1:1 heading levels — when you change one, change the other
7
+ (a new section must be added on both sides). npm / the DSH marketplace show README.md (Chinese) by default. -->
8
+
9
+ <p align="center"><a href="README.md">简体中文(默认)</a> · <b>English</b></p>
10
+
3
11
  <p align="center">
4
12
  <img src="https://img.shields.io/badge/DeepSeek%20Harness-plugin-3B82F6" alt="DSH plugin" />
5
13
  <img src="https://img.shields.io/npm/v/memory-eternal" alt="npm version" />
@@ -133,14 +141,17 @@ Three concepts, don't mix them:
133
141
 
134
142
  - **How the web server stays alive** (`autoWebMode`) → `init`=pull once at DSH start (default); `interval`=DSH in-process timer probes & auto-restarts (0 extra memory); `manual`=fully manual, only from `dsh-memory open`.
135
143
  - **Watchdog process** (`watchdogAutoSpawn`, default on) → a **standalone** node process that can pull web up even after DSH exits (~+47 MB RAM). Only turn on for 7×24 keep-alive.
144
+ - **Version-drift self-heal** (`autoRestartOnDrift`, default on) → the resident web **outlives DSH**: an upgrade only swaps files on disk, so the process on the port keeps running the code it loaded at startup (that is where "still on the old version after starting dsh" comes from). With this on, DSH asks **the port** which version it really serves and, on a mismatch, restarts the resident instance on the new code. Turn it off to only get a warning — the **Restart resident instance** button in Plugin info stays available.
136
145
  - **Auto-mount MCP** (`autoMcpSetup`, default off) → whether to auto-write MCP into Claude Code/Codex/Cursor config. Off = don't touch your machine config; run `dsh-memory setup` manually when needed.
137
146
 
138
147
  **Change these**: Vault left rail "Memory Config" (or DSH Settings → Memory) → edit the table and save; `autoWebMode` / `watchdogAutoSpawn` need a DSH restart.
139
148
 
140
- > ⚠️ **Same port is never auto-replaced (#19)**: the watchdog is a **standalone process we deliberately never kill** (several sessions share one). So turning `watchdogAutoSpawn` off does **not** stop the running one, and changing `webCheckIntervalMs` / `webMaxRestart` has no effect on it.
149
+ > ⚠️ **A config change never auto-replaces the instance on the same port (#19)**: the watchdog is a **standalone process we deliberately never kill** (several sessions share one). So turning `watchdogAutoSpawn` off does **not** stop the running one, and changing `webCheckIntervalMs` / `webMaxRestart` has no effect on it.
141
150
  > Stop or upgrade it explicitly with `dsh-memory stop [--port N]` / `dsh-memory restart [--port N]` (`stop` also stops the **web it spawned**, so no orphan keeps holding the port); `dsh-memory status` shows the resident pid, port, start time, whether its **version lags the installed package**, and separately the version **actually served on the port** (the lock's version is only the watchdog's self-report).
142
151
  >
143
152
  > ⚠️ **After upgrading, trust the served version (#23)**: `restart` no longer trusts the lock's `webPid` alone — it reaps whoever holds the port (including web processes the lock never registered; non-memory-eternal processes are only reported, never killed), and after spawning it verifies that the **served version equals the installed version**, exiting non-zero instead of printing "started" and walking away.
153
+ >
154
+ > ✅ **"Upgraded but still on the old version" now has two ways out (#19/#23)**: ① `autoRestartOnDrift` (default on) replaces a stale resident instance **automatically** when DSH activates; ② UI → Plugin info shows a **🔄 Restart resident instance** button whenever it detects drift (the server side is the same implementation as `dsh-memory restart --port N`: reap the old instance → take the lock → pull up the new web → verify the served version). Note the "running" chip means **the process serving this page** — the resident web on the standalone page, the host inside DSH Settings; the panel also lists the **resident instance** version, so you can tell at a glance which one to restart.
144
155
 
145
156
  **MCP is a protocol, not a resident service**: the agent spawns it per session and exits when done — no "auto-start on boot" concept.
146
157
 
@@ -204,7 +215,18 @@ Resolution order: `MEMORY_VAULT_DIR` env → the `path` of the `vaultProfiles` e
204
215
  |---|---|---|
205
216
  | Keep-alive `autoWebMode` | init | `init`=open web once at DSH start; `interval`=periodically check & restart if dead; `manual`=fully manual |
206
217
  | Watchdog `watchdogAutoSpawn` | on | a **standalone process** keeps web alive (+47 MB). Personal use can turn off — but an existing instance is **not** stopped automatically, run `dsh-memory stop` (#19) |
218
+ | Drift self-heal `autoRestartOnDrift` | on | when DSH activates and the version **served on the port ≠ the version on disk** (resident web still on old code after an upgrade), restart the resident instance on the new code. Off = warn only; the **🔄 Restart resident instance** button still works (#19/#23) |
207
219
  | Auto-mount MCP `autoMcpSetup` | off | **lets Claude Code / Codex / Cursor use your memory**. On = auto-configures them; Off = never touches your config, run `dsh-memory setup` manually |
220
+ | Credential hint `secretHint` | empty | put your own one-liner here (e.g. "when you need an API key/token, look up the secret directory in memory first"); it is **injected into every session's systemPrompt**. Memory only stores the **directory** (names / locations / how to use) — secret **values** never enter the vault (see "Where secrets live" below) |
221
+
222
+ > 🔑 **Where secrets live (how to use `secretHint`)**: keep the **values** in local credential stores
223
+ > (env vars / permission-tightened files); keep only a **directory card** in memory (which keys exist,
224
+ > where they live, what they are for, rotation status) and use `secretHint` so the agent remembers to
225
+ > look it up every session. Why values must not be stored: a card's `summary` is just the first 200
226
+ > chars of its body, and **recall sends the first 130 chars by default** (`recallSummaryLen`) — a value
227
+ > in the vault would travel into the model context on every recall, plus into `/export`, backups and
228
+ > multi-machine sync. `dsh-memory setup` registers a `PreToolUse` guard in Claude Code that **denies**
229
+ > direct reads of `.credentials.yaml` / `.env` / `*-token` and points at the directory card instead.
208
230
 
209
231
  > 💰 **To save money**: turn `Distill cards` off, lower `Distill output cap`, raise `Recall min score`.
210
232
 
@@ -283,6 +305,10 @@ npm test # unit tests: vault dedup/retrieval/graph + capture pipeline + A
283
305
  npm run build # builds lib/client.js (DSH embed) + web/app.js (standalone web bundle)
284
306
  ```
285
307
 
308
+ > 📄 **The README ships as two separate files**: `README.md` (Simplified Chinese, the **default** —
309
+ > this is what npm and the plugin marketplace render) and `README.en.md` (English). Heading levels are
310
+ > 1:1 between them, so **a change in one must be mirrored in the other** (add new sections on both sides).
311
+
286
312
  ---
287
313
 
288
314
  ## 🐞 Reporting a bug
@@ -318,6 +344,14 @@ Diagnostics:
318
344
  <paste the diagnostics copied from the in-app Report-a-bug dialog>
319
345
  ```
320
346
 
347
+ ### Self-diagnostics (paste these when reporting — triage gets much faster)
348
+
349
+ ```bash
350
+ dsh plugin list # plugin version
351
+ node -v # Node version
352
+ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone web alive?
353
+ ```
354
+
321
355
  ---
322
356
 
323
357
  ## 📋 Changelog
@@ -327,6 +361,8 @@ Diagnostics:
327
361
 
328
362
  | Version | Date | Highlights |
329
363
  |---|---|---|
364
+ | **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 **282 checks** |
365
+ | **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** |
330
366
  | **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** |
331
367
  | **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** |
332
368
  | **v0.10.4** | 2026-10-07 | **Four "looks fine, silently wrong" issues fixed at once (#21 / #22 / #23 / #24).** ① **A save misreported by a host guard is no longer treated as failed (#21)**: the capability guard in hosts such as dsh-tui throws `root.events.emit is unavailable from a plugin activation` from the `describe()` call **inside** `settings.update`, even though the write has already been committed — the old code only asked "did it throw", so an **already-applied** change was retried 5 times, marked `dropped` and logged as a failure. New `applyPatchVerified` **reads the config back** after a throw (predicate `patchApplied`, shallow compare) and logs one line while treating it as success; drain and the standalone web share that path, and the standalone web's `POST /config` no longer claims "all good" unconditionally: it waits up to 800 ms for the pending file to be consumed and returns `pendingOutcome: applied / failed / queued` with matching wording (`waitPendingOutcome`). ② **`/cards` status no longer silently misaligns (#22)**: `status=deleted` now really lists the **recycle bin** (the numbers match `/recycle/list`), and an unrecognized value returns `unknownStatus: true` plus `appliedStatus` / `requestedStatus` echoes (the fallback to approved stays for old callers, but it is no longer silent). ③ **After an upgrade, trust the version actually served on the port (#23)**: `/overview` gains the `version` loaded by the **serving process**; `dsh-memory status` reports the **port's actual served version** and says "upgrade not in effect" when it differs (the lock file's version is only the watchdog's self-report; when the port holds a **pre-0.10.4** web nothing can be probed because its `/overview` has no `version` field yet, and the tool then says exactly that — "this plugin's web, but it does not report a version, i.e. an old build" — instead of wrongly claiming no service is responding); `stop` re-checks the port occupant afterwards and warns if it is still taken; `restart` no longer trusts only the webPid recorded in the lock — it falls back to the **port occupant** to reap an old web the lock never registered (only after confirming from the command line that it really is **this plugin's** web — the `memory-eternal` package name, or the absolute path of this plugin's own `lib/web.js`; someone else's `node web.js` is merely warned about, since killing an unrelated process that happens to hold that port is worse than not killing at all), aborts if the port never frees up, and self-checks "port-served version = local version" after startup, exiting non-zero on a mismatch (no more "started" and then silence). ④ **Distillation no longer misreads bare control characters inside strings as unparseable (#24)**: when the model writes a markdown body with real U+000A newlines (braces balanced, `looksTruncatedJson` false), the old code always failed `JSON.parse` and dropped the whole card into a raw-text fallback. New `repairJsonControlChars` escapes bare control characters **only inside strings** (`\n` `\r` `\t` `\b` `\f`, otherwise `\u00XX`; newlines outside strings are legal JSON whitespace and stay untouched), and candidates are tried as "as-is → sliced `{ … }` → each one's repaired form"; parse failures no longer swallow the raw `JSON.parse` error, and the excerpt now keeps **both head and tail** (`describeOutputExcerpt`; the old `slice(0,160)` made output cut mid-`"body":` look like a broken stream); the max-tokens retry went from "double once" to `maxTokenLadder` **doubling all the way to the schema ceiling** (1200 → 2400 → 4000, no longer stopping at 2400). New `tests/issue-fixes-2.test.mjs` (21 checks); `npm test` now runs **229 checks** |
package/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # 🧠 memory-eternal — 给 AI 装「第二大脑」
2
2
 
3
+ [![HOL Guard](https://img.shields.io/endpoint?url=https%3A%2F%2Fhol.org%2Fapi%2Fregistry%2Fbadges%2Fplugin%3Fslug%3Deternalnight996%252Fmemory-eternal%26metric%3Dtrust)](https://hol.org/registry/plugins/eternalnight996%2Fmemory-eternal)
4
+
5
+ <!-- 语言 / Language:本文件是**默认**(简体中文)README;英文镜像见 README.en.md。
6
+ 两份是**独立文件**、标题层级一一对应;改一处请同步另一处(新增一节要两边都加)。
7
+ npm / DSH 插件市场默认展示本文件(中文)。 -->
8
+
9
+ <p align="center"><b>简体中文(默认)</b> · <a href="README.en.md">English</a></p>
10
+
3
11
  <p align="center">
4
12
  <img src="https://img.shields.io/badge/DeepSeek%20Harness-plugin-3B82F6" alt="DSH plugin" />
5
13
  <img src="https://img.shields.io/npm/v/memory-eternal" alt="npm version" />
@@ -144,11 +152,12 @@ dsh-memory audit reject <卡片路径...> --reason "..." # 人工批量驳回
144
152
 
145
153
  - **web server 怎么保活**(`autoWebMode`)→ `init`=DSH 启动时拉一次(默认);`interval`=DSH 进程内定时探活自动拉起(0 额外内存);`manual`=全手动只从 `dsh-memory open` 起。
146
154
  - **看门狗进程**(`watchdogAutoSpawn`,默认开)→ 一个**独立** node 进程,DSH 退出了它也能拉起 web(约 +47 MB 内存)。只在要 7×24 保活时开。
155
+ - **版本漂移自愈**(`autoRestartOnDrift`,默认开)→ 常驻 web **比 DSH 活得久**:升级只换磁盘文件,端口上那个进程仍跑着启动时加载的旧代码(「启动 dsh 后运行中还是旧版本」就是这么来的)。开着它,DSH 激活时会**问端口上真正服务的版本**,与磁盘不一致就用新代码重启常驻实例;关掉则只告警,需要时在「插件信息」点**重启常驻实例**。
147
156
  - **自动挂载 MCP**(`autoMcpSetup`,默认关)→ 是否自动把 MCP 写进 Claude Code/Codex/Cursor 配置。关 = 不碰你本机配置文件,需要时手动 `dsh-memory setup`。
148
157
 
149
158
  **改这些**:记忆库左栏「记忆配置」(或 DSH 设置 → 记忆)→ 表格里改,点「保存配置」;`autoWebMode`/`watchdogAutoSpawn` 需重启 DSH 生效。
150
159
 
151
- > ⚠️ **同端口不会自动替换(#19)**:看门狗是**独立进程且故意不杀**(多会话共用一个),所以
160
+ > ⚠️ **同端口不会因配置改动而自动替换(#19)**:看门狗是**独立进程且故意不杀**(多会话共用一个),所以
152
161
  > 关闭 `watchdogAutoSpawn` **不会**停掉已经在跑的那个,改 `webCheckIntervalMs`/`webMaxRestart` 也不会生效。
153
162
  > 要停止 / 换新版请显式执行 `dsh-memory stop [--port N]` 与 `dsh-memory restart [--port N]`(`stop` 会**连它拉起的 web 一起停**,不留占端口的孤儿);
154
163
  > `dsh-memory status` 会显示常驻实例的 pid、端口、启动时间与**版本是否落后于本机安装的包**,并单独报出**端口上真正服务的版本**
@@ -157,6 +166,12 @@ dsh-memory audit reject <卡片路径...> --reason "..." # 人工批量驳回
157
166
  > ⚠️ **升级后请以「端口实际服务版本」为准(#23)**:`restart` 现在不只信锁里登记的 web 子进程,
158
167
  > 还会按**端口占用者**兜底清理(锁里没登记的旧 web 也会被识别并收掉,非本插件的进程只告警不动手),
159
168
  > 并在起完之后自检「端口上服务的版本 = 本机版本」——对不上就以非 0 退出码报失败,不再打印「已启动」就完事。
169
+ >
170
+ > ✅ **「更新了却还是旧版本」现在有两条出路(#19/#23)**:① 默认开的 `autoRestartOnDrift` 在 DSH 启动时
171
+ > **自动**替换旧常驻实例;② 面板「插件信息」里发现版本漂移时给一个**🔄 重启常驻实例**按钮(服务端即
172
+ > `dsh-memory restart --port N` 的同一实现:收旧实例 → 抢锁 → 拉起新 web → 自检端口版本)。
173
+ > 注意「运行中」那一格指的是**服务本页的进程**:独立 Web 页里就是常驻 web,DSH 设置页里是宿主 ——
174
+ > 面板会同时列出**常驻实例**的版本,两者不同时一眼能看出该重启谁。
160
175
 
161
176
  **MCP 是协议不是常驻服务**:agent 开会话才 spawn,用完即退,没有「开机自启」一说。
162
177
 
@@ -220,7 +235,16 @@ dsh-memory audit reject <卡片路径...> --reason "..." # 人工批量驳回
220
235
  |---|---|---|
221
236
  | 保活模式 `autoWebMode` | init | `init`=DSH 启动时开一次 web;`interval`=定时检查挂了自动重启;`manual`=全靠手动 |
222
237
  | 看门狗 `watchdogAutoSpawn` | 开 | 后台一个**独立进程**保证 web 不死(+47 MB 内存)。个人用可关——但关闭后**既有实例不会自动停**,需 `dsh-memory stop`(#19) |
238
+ | 漂移自愈 `autoRestartOnDrift` | 开 | DSH 激活时发现**端口上服务的版本 ≠ 磁盘版本**(升级后常驻 web 还是旧代码)就用新代码重启常驻实例。关掉只告警,仍可在「插件信息」点**🔄 重启常驻实例**(#19/#23) |
223
239
  | 自动挂载 MCP `autoMcpSetup` | 关 | **让 Claude Code / Codex / Cursor 也能用你的记忆库**。开=自动配好它们;关=不碰你电脑配置,手动跑 `dsh-memory setup` |
240
+ | 凭证提示 `secretHint` | 空 | 填一句自己的约定(例如「需要 API key/token 时先查记忆里的密钥目录」),会**注入每个会话的 systemPrompt**。记忆库里只存**目录**(名字/位置/取用方式),密钥的**值**不入库 —— 见下方「密钥怎么放」 |
241
+
242
+ > 🔑 **密钥怎么放(`secretHint` 的用法)**:把**值**留在本机凭据里(环境变量 / 密钥文件,权限收紧),
243
+ > 记忆库里只留一张**目录卡**(有哪些键、放在哪、干什么用、轮换状态),再用 `secretHint` 让 agent
244
+ > 每次会话都想到去查这张卡。为什么值不能入库:卡片 `summary` 就是正文开头,**召回默认就带 130 字**
245
+ > (`recallSummaryLen`),值一入库就随每次相关召回进入模型上下文;`/export`、备份、多机同步也会带上。
246
+ > `dsh-memory setup` 会在 Claude Code 里注册一个 `PreToolUse` 守卫:直接读
247
+ > `.credentials.yaml` / `.env` / `*-token` 会被拒绝并指路到目录卡。
224
248
 
225
249
  > 💰 **想省钱**:把「蒸馏知识卡」关掉、调低「蒸馏输出上限」、调高「召回相关性阈值」。
226
250
 
@@ -309,6 +333,9 @@ npm test # 单元测试:vault 去重/检索/图谱 + capture 管线 + A
309
333
  npm run build # 构建 lib/client.js(DSH 内嵌)+ web/app.js(独立 web bundle)
310
334
  ```
311
335
 
336
+ > 📄 **README 是中英两份独立文件**:`README.md`(简体中文,**默认**,npm / 插件市场展示这一份)
337
+ > 与 `README.en.md`(English)。两份标题层级一一对应,**改一处要同步另一处**(新增一节两边都加)。
338
+
312
339
  ---
313
340
 
314
341
  ## 🐞 反馈问题
@@ -366,6 +393,8 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
366
393
 
367
394
  | 版本 | 日期 | 关键改动 |
368
395
  |---|---|---|
396
+ | **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` 共 **282 项** |
397
+ | **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 项** |
369
398
  | **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 项** |
370
399
  | **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 项** |
371
400
  | **v0.10.4** | 2026-10-07 | **四个「看着正常、实际静默错位」的 issue 集中修复(#21 / #22 / #23 / #24)**。① **宿主守卫误报的保存不再被当成失败(#21)**:dsh-tui 一类宿主的能力守卫会在 `settings.update` **内部**的 `describe()` 上抛 `root.events.emit is unavailable from a plugin activation`,而写入其实已经提交 —— 旧实现只认「有没有抛错」,会把**已生效**的改动重试 5 次后标成 `dropped`、日志里还报失败。新增 `applyPatchVerified`(抛错后**回读确认**,判据 `patchApplied` 浅比较,命中则记一行日志按成功处理),drain 与独立 Web 端共用这条路径;独立 Web 的 `POST /config` 也不再无条件回「一切正常」:等最多 800ms 回读 pending 文件,回 `pendingOutcome: applied / failed / queued` 并给对应措辞(`waitPendingOutcome`)。② **`/cards` 的 status 不再静默错位(#22)**:`status=deleted` 现在真的列**回收站**(与 `/recycle/list` 的数字对得上),未知取值显式回 `unknownStatus: true` 并回显 `appliedStatus` / `requestedStatus`(回落 approved 的老行为保留,但不再沉默)。③ **升级后以「端口上真正服务的版本」为准(#23)**:`/overview` 新增**当前服务进程**加载的 `version`;`dsh-memory status` 单独报「端口实际服务版本」,对不上就直说「升级尚未生效」(锁里的版本号只是 watchdog 自述;端口上是 **v0.10.4 之前**的旧 web 时探不到版本 —— 它的 `/overview` 还没有 `version` 字段 —— 这时会明说「是本插件的 web 但不会自报版本,即旧版」,而不是误报「没有服务在响应」);`stop` 停完再问一次端口占用者、仍被占就告警;`restart` 不再只信锁里登记的 webPid —— 按**端口占用者**兜底收掉锁里没登记的旧 web(先用命令行确认**确实是本插件的 web**(包名 `memory-eternal`,或本插件自己的 `lib/web.js` 绝对路径)才动手,别人的 `node web.js` 只告警不动手 —— 收紧这条判据是刻意的:误杀别人占着该端口的进程,比不杀更糟),端口没空出来就中止重启,起完自检「端口服务版本 = 本机版本」,对不上以非 0 退出(不再打印「已启动」就完事)。④ **蒸馏解析不再把「字符串里的裸控制字符」误判成无法解析(#24)**:模型把 markdown 正文的换行写成真实 U+000A(括号闭合、`looksTruncatedJson` 为 false)时,旧实现必然 `JSON.parse` 失败 → 整卡退成原文卡;新增 `repairJsonControlChars` **只在字符串内部**把裸控制字符转成合法转义(`\n` `\r` `\t` `\b` `\f`,其余 `\u00XX`;字符串外的换行是合法 JSON 空白,一律不动),候选串顺序为「原样 → 截 `{ … }` → 各自的已修复版」;解析失败的消息不再吞掉 `JSON.parse` 原始报错,现场摘要改为**首尾各留一段**(`describeOutputExcerpt`,旧的 `slice(0,160)` 恰好把「被切在 `"body":` 半截处」显示得像输出断了);撞输出上限的重试从「只翻倍一次」改为 `maxTokenLadder` **一路翻倍到 schema 上限**(1200 → 2400 → 4000,不再止步 2400)。新增 `tests/issue-fixes-2.test.mjs`(21 项),`npm test` 共 **229 项** |
@@ -110,10 +110,21 @@ async function main() {
110
110
  const { url } = await ensureWebServer({ port, vaultRoot: path.resolve(defaultVaultDir()) })
111
111
  console.log(url)
112
112
  const plat = process.platform
113
- const { exec } = await import('node:child_process')
114
- if (plat === 'win32') exec(`start "" "${url}"`)
115
- else if (plat === 'darwin') exec(`open "${url}"`)
116
- else exec(`xdg-open "${url}"`)
113
+ // 用 spawn + 参数数组,而不是把值拼进 shell 命令字符串:
114
+ // 后者正是扫描器(HOL Guard / ai-plugin-scanner)判定 SHELL_INJECTION_PATTERN 的形态,
115
+ // 而且一旦 url 里有空格/引号就会被 shell 重新解析。参数数组不经 shell,天然免疫。
116
+ // (注意:扫描器是**按文本正则**扫的,连注释里的示例写法也会命中,这里就不写示例了。)
117
+ const { spawn } = await import('node:child_process')
118
+ const launch = (cmd, args) => {
119
+ try {
120
+ const child = spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true })
121
+ child.on('error', () => { /* 打不开浏览器不影响 ensureWeb 的结果 */ })
122
+ child.unref()
123
+ } catch { /* 同上 */ }
124
+ }
125
+ if (plat === 'win32') launch('cmd', ['/c', 'start', '', url])
126
+ else if (plat === 'darwin') launch('open', [url])
127
+ else launch('xdg-open', [url])
117
128
  return
118
129
  }
119
130
  case 'mcp': {
@@ -167,56 +178,31 @@ async function main() {
167
178
  }
168
179
  case 'restart': {
169
180
  // 显式替换常驻实例(#19):同端口已有实例时新进程只会让位,所以必须先停再起。
170
- const { stopWatchdogs, findPortListener, waitForServedVersion, probeServedVersion, looksLikeOurWeb, currentPkgVersion } = await import('../lib/watchdog.js')
171
- const { nodeBinary, childEnv } = await import('../lib/node-bin.js')
181
+ // 统一走 lib/watchdog.js 的 restartResident —— 宿主启动自愈与面板「重启常驻实例」按钮
182
+ // 用的是同一个实现:spawn 一个 detached 的 `watchdog.js --replace` 助手收旧实例并接管,
183
+ // 再自检「端口上真正服务的版本 == 本机版本」(issue #23 建议 2)。
184
+ const { restartResident, watchdogStatus } = await import('../lib/watchdog.js')
172
185
  const { defaultVaultDir } = await import('../lib/capture-run.js')
173
- const { spawn } = await import('node:child_process')
174
- const { fileURLToPath } = await import('node:url')
175
186
  const port = Number(argOf('--port')) || 7999
176
187
  const interval = Number(argOf('--interval')) || 5000
177
188
  const maxRestart = Number(argOf('--max-restart')) || 10
178
189
  const vaultRoot = argOf('--vault') ? path.resolve(argOf('--vault')) : defaultVaultDir()
179
- // 停的时候不只信锁里登记的 webPid —— 锁可能没写 / 写了却枚举不到,正是
180
- // 「restart 报成功、端口上还是旧代码」的漏网处(issue #23):直接按端口占用者兜底收掉。
181
- const stopped = await stopWatchdogs({ port, checkPort: true, forcePortOccupant: true })
182
- if (stopped.failed.length) { console.error(`旧实例未能停止:pid ${stopped.failed.join(', ')},放弃重启`); process.exitCode = 1; return }
183
- if (stopped.stopped.length) console.log(`已停止旧实例:pid ${stopped.stopped.join(', ')}`)
184
- if (stopped.webStopped && stopped.webStopped.length) console.log(`已停止旧实例拉起的 web:pid ${stopped.webStopped.join(', ')}`)
185
- if (stopped.portStopped && stopped.portStopped.length) console.log(`已停止占着端口 ${port} 的旧 web(锁里没登记):pid ${stopped.portStopped.join(', ')}`)
186
- for (const w of stopped.warnings || []) console.error('⚠ ' + w)
187
- // 端口必须真的空出来:否则新 web 只能退到 port+1,旧代码继续在目标端口上服务
188
- let left = await findPortListener(port)
189
- if (left && left.pid && looksLikeOurWeb(left.command)) {
190
- try { process.kill(left.pid, 'SIGTERM') } catch { /* 无权限只能报错 */ }
191
- const releaseDeadline = Date.now() + 5000
192
- while (Date.now() < releaseDeadline && (left = await findPortListener(port))) await new Promise((r) => setTimeout(r, 250))
190
+ const before = watchdogStatus({ port }).watchdogs.filter((w) => w.alive)
191
+ if (before.length) {
192
+ const webs = before.filter((w) => w.webPid).map((w) => w.webPid)
193
+ console.log(`将替换常驻实例:pid ${before.map((w) => w.pid).join(', ')}${webs.length ? `(它拉起的 web:pid ${webs.join(', ')})` : ''}`)
193
194
  }
194
- left = await findPortListener(port)
195
- if (left && left.pid) {
196
- console.error(`端口 ${port} 仍被 pid ${left.pid} 占用(${left.command || '命令行未知'}),新实例无法接管该端口,已中止重启。`)
197
- console.error(`请先结束该进程,或用 --port 换一个端口:dsh-memory restart --port <其它端口>`)
195
+ // 助手拿到的是「旧实例留下的孤儿也不放过」的口径(按端口占用者兜底收掉,见 #23)
196
+ const out = await restartResident({ port, vaultRoot, interval, maxRestart })
197
+ if (!out.ok) {
198
+ console.error(`替换失败(${out.stage}):${out.error || '未知原因'}`)
199
+ console.error(`排查:dsh-memory status --port ${port};若端口被别的进程占着,先结束它再 restart;`)
200
+ console.error(' 接管结论也会写进「自动沉淀日志」(设置 → 记忆 → 用量/今日)里。')
198
201
  process.exitCode = 1
199
202
  return
200
203
  }
201
- const bin = nodeBinary()
202
- const script = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'lib', 'watchdog.js')
203
- const wd = spawn(bin, [script, '--port', String(port), '--interval', String(interval), '--max-restart', String(maxRestart), '--vault', vaultRoot], {
204
- detached: true, stdio: 'ignore', env: childEnv({ MEMORY_VAULT_DIR: vaultRoot }), windowsHide: true,
205
- })
206
- wd.unref()
207
- console.log(`已在后台启动新看门狗 pid=${wd.pid} port=${port} vault=${vaultRoot}`)
208
- // 起完必须自检「端口上真正服务的版本」(issue #23 建议 2):只看 spawn 成功就报
209
- // 「已启动」,正是升级静默不生效时骗人的那一步 —— 端口上服务的版本必须等于本机版本。
210
- const expect = currentPkgVersion()
211
- const check = await waitForServedVersion(port, expect, { timeoutMs: 20000 })
212
- if (check.ok) {
213
- console.log(`自检通过:端口 ${port} 正在服务 v${check.version}(等待 ${Math.round(check.waitedMs / 1000)}s)`)
214
- } else {
215
- const served = check.version || (await probeServedVersion(port)) || ''
216
- console.error(`自检失败:端口 ${port} 上服务的版本是 ${served || '(无响应)'},本机是 v${expect || '?'} —— 升级可能未生效。`)
217
- console.error(`排查:dsh-memory status --port ${port};若端口被别的进程占着,先结束它再 restart。`)
218
- process.exitCode = 1
219
- }
204
+ console.log(`已在后台启动新看门狗 pid=${out.helperPid} port=${port} vault=${vaultRoot}`)
205
+ console.log(`自检通过:端口 ${port} 正在服务 v${out.served}(等待 ${Math.round(out.waitedMs / 1000)}s)`)
220
206
  return
221
207
  }
222
208
  case 'audit': {
@@ -1,59 +1,59 @@
1
- # 记忆核心(memory-eternal)· MCP 对外通道示例 overlay
2
- #
3
- # 用途:把 memory-eternal 的 MCP stdio 服务挂进 DSH —— 让 **DSH 自己**也能通过官方
4
- # `@deepseek-ai/dsh-mcp-client` 使用记忆库(与 Claude Code / Codex / Cursor 走同一条协议路径)。
5
- #
6
- # 什么时候需要它:
7
- # · 你没装 memory-eternal 插件本体,只想用它的记忆库 → 用这个 overlay;
8
- # · 你已经装了插件(推荐):插件是「一等公民」,这条 MCP 通道**不需要**,因为插件
9
- # 自带 memory_recall 工具与系统提示段,还能拿到会话事件做自动沉淀。
10
- #
11
- # 用法(示例文件不在交付包里自动生效,必须显式 --patch 或合并进用户 patch 层):
12
- # dsh web --patch "<仓库>/examples/mcp-memory/memory-eternal.cordis.yml"
13
- # 或把下面的 insert 合并进 $DSH_HOME/cordis.patch.yml(本机所有 profile 生效)
14
- # 或 $DSH_HOME/profiles/<name>/cordis.patch.yml(只对该 profile 生效)
15
- #
16
- # 前置条件(二进制已随包提供,无需额外安装):
17
- # · Node 22+(memory-eternal engines 要求)
18
- # · 全局安装或 profile 内安装 memory-eternal(提供 bin/dsh-memory.mjs)
19
- # 全局:npm i -g memory-eternal
20
- # profile 内:cd ~/.dsh/profiles/<name> && pnpm add memory-eternal
21
- # 下面的 args 用的是「profile 内安装」路径;全局安装请改成
22
- # args: ['dsh-memory', 'mcp'](Windows 下为 dsh-memory.cmd)。
23
-
24
- - insert:
25
- - id: memory-eternal-mcp
26
- name: '@deepseek-ai/dsh-mcp-client'
27
- config:
28
- serverName: eternal
29
- transport: stdio
30
- command: node
31
- args:
32
- # 如果是全局安装,改成 ['dsh-memory', 'mcp']
33
- - 'C:/Users/<你>/.dsh/profiles/web/node_modules/memory-eternal/bin/dsh-memory.mjs'
34
- - 'mcp'
35
- # 注意:stdio 桥接会主动**剥离名字像凭据的环境变量以及所有 DSH_* 变量**,其余继承。
36
- # 因此:
37
- # · 想让 MCP 进程用哪个库,显式在这里写 MEMORY_VAULT_DIR(不要依赖继承);
38
- # · 想让 MCP 的 memory_capture 具备「LLM 蒸馏」能力,需要在这里显式传 MEMORY_LLM_*
39
- # (这类变量名字含 KEY/TOKEN,很可能被桥接当凭据剥掉);
40
- # · 不传就是「原文卡」降级模式,仍然可用,只是不压缩。
41
- env:
42
- # 留空 = 用插件自己的解析顺序(共享配置 memory-eternal-config.json → 默认 ~/.dsh/memory-vault)
43
- MEMORY_VAULT_DIR: ''
44
- # 可选:MCP 侧蒸馏用的 OpenAI 兼容端点
45
- # MEMORY_LLM_BASE_URL: 'https://api.deepseek.com'
46
- # MEMORY_LLM_MODEL: 'deepseek-chat'
47
- # MEMORY_LLM_API_KEY: '<在此填 key>'
48
- cwd: !!js process.cwd()
49
-
50
- # 挂上之后 DSH 侧会看到这些工具(前缀由 serverName 决定):
51
- # mcp__eternal__memory_recall —— 检索记忆(CJK 整词 + 字符 bigram,只读主库/approved)
52
- # mcp__eternal__memory_capture —— 手动沉淀一段内容(走审核闸门,默认落隔离区 pending)
53
- # mcp__eternal__memory_stats —— 记忆库概览
54
- #
55
- # 验证(与官方文档同一套三步骤):
56
- # 1) 会话 A:让模型调用 mcp__eternal__memory_capture 写入一个唯一值;
57
- # 2) 新建会话 B(不必重启宿主):问「我的验证饮料是什么?查记忆」→ 应调用 memory_recall 并返回该值;
58
- # 3) 继续在 B 里用该偏好做一次推断。
59
- # 本仓库 tests/e2e-recall.test.mjs 是同一套步骤的自动化版本(走 vault API)。
1
+ # 记忆核心(memory-eternal)· MCP 对外通道示例 overlay
2
+ #
3
+ # 用途:把 memory-eternal 的 MCP stdio 服务挂进 DSH —— 让 **DSH 自己**也能通过官方
4
+ # `@deepseek-ai/dsh-mcp-client` 使用记忆库(与 Claude Code / Codex / Cursor 走同一条协议路径)。
5
+ #
6
+ # 什么时候需要它:
7
+ # · 你没装 memory-eternal 插件本体,只想用它的记忆库 → 用这个 overlay;
8
+ # · 你已经装了插件(推荐):插件是「一等公民」,这条 MCP 通道**不需要**,因为插件
9
+ # 自带 memory_recall 工具与系统提示段,还能拿到会话事件做自动沉淀。
10
+ #
11
+ # 用法(示例文件不在交付包里自动生效,必须显式 --patch 或合并进用户 patch 层):
12
+ # dsh web --patch "<仓库>/examples/mcp-memory/memory-eternal.cordis.yml"
13
+ # 或把下面的 insert 合并进 $DSH_HOME/cordis.patch.yml(本机所有 profile 生效)
14
+ # 或 $DSH_HOME/profiles/<name>/cordis.patch.yml(只对该 profile 生效)
15
+ #
16
+ # 前置条件(二进制已随包提供,无需额外安装):
17
+ # · Node 22+(memory-eternal engines 要求)
18
+ # · 全局安装或 profile 内安装 memory-eternal(提供 bin/dsh-memory.mjs)
19
+ # 全局:npm i -g memory-eternal
20
+ # profile 内:cd ~/.dsh/profiles/<name> && pnpm add memory-eternal
21
+ # 下面的 args 用的是「profile 内安装」路径;全局安装请改成
22
+ # args: ['dsh-memory', 'mcp'](Windows 下为 dsh-memory.cmd)。
23
+
24
+ - insert:
25
+ - id: memory-eternal-mcp
26
+ name: '@deepseek-ai/dsh-mcp-client'
27
+ config:
28
+ serverName: eternal
29
+ transport: stdio
30
+ command: node
31
+ args:
32
+ # 如果是全局安装,改成 ['dsh-memory', 'mcp']
33
+ - 'C:/Users/<你>/.dsh/profiles/web/node_modules/memory-eternal/bin/dsh-memory.mjs'
34
+ - 'mcp'
35
+ # 注意:stdio 桥接会主动**剥离名字像凭据的环境变量以及所有 DSH_* 变量**,其余继承。
36
+ # 因此:
37
+ # · 想让 MCP 进程用哪个库,显式在这里写 MEMORY_VAULT_DIR(不要依赖继承);
38
+ # · 想让 MCP 的 memory_capture 具备「LLM 蒸馏」能力,需要在这里显式传 MEMORY_LLM_*
39
+ # (这类变量名字含 KEY/TOKEN,很可能被桥接当凭据剥掉);
40
+ # · 不传就是「原文卡」降级模式,仍然可用,只是不压缩。
41
+ env:
42
+ # 留空 = 用插件自己的解析顺序(共享配置 memory-eternal-config.json → 默认 ~/.dsh/memory-vault)
43
+ MEMORY_VAULT_DIR: ''
44
+ # 可选:MCP 侧蒸馏用的 OpenAI 兼容端点
45
+ # MEMORY_LLM_BASE_URL: 'https://api.deepseek.com'
46
+ # MEMORY_LLM_MODEL: 'deepseek-chat'
47
+ # MEMORY_LLM_API_KEY: '<在此填 key>'
48
+ cwd: !!js process.cwd()
49
+
50
+ # 挂上之后 DSH 侧会看到这些工具(前缀由 serverName 决定):
51
+ # mcp__eternal__memory_recall —— 检索记忆(CJK 整词 + 字符 bigram,只读主库/approved)
52
+ # mcp__eternal__memory_capture —— 手动沉淀一段内容(走审核闸门,默认落隔离区 pending)
53
+ # mcp__eternal__memory_stats —— 记忆库概览
54
+ #
55
+ # 验证(与官方文档同一套三步骤):
56
+ # 1) 会话 A:让模型调用 mcp__eternal__memory_capture 写入一个唯一值;
57
+ # 2) 新建会话 B(不必重启宿主):问「我的验证饮料是什么?查记忆」→ 应调用 memory_recall 并返回该值;
58
+ # 3) 继续在 B 里用该偏好做一次推断。
59
+ # 本仓库 tests/e2e-recall.test.mjs 是同一套步骤的自动化版本(走 vault API)。