memory-eternal 0.10.29 → 0.10.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.en.md +6 -3
- package/README.md +6 -3
- package/bin/dsh-memory.mjs +10 -0
- package/index.js +23 -0
- package/lib/align.js +11 -2
- package/lib/api.js +5 -0
- package/lib/client.js +17 -17
- package/lib/resident-state.js +61 -0
- package/lib/vault.js +40 -6
- package/lib/watchdog.js +30 -10
- package/package.json +2 -2
- package/web/app.js +15 -15
|
@@ -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.
|
|
4
|
+
"version": "0.10.32",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.en.md
CHANGED
|
@@ -361,14 +361,17 @@ 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.
|
|
365
|
-
| **v0.10.
|
|
364
|
+
| **v0.10.32** | 2026-10-15 | **"The resident instance cannot be replaced" becomes a long-lived state instead of a retry on every boot (issue #30, fifth retest round: the steady state on the A: drive machine)**. The reporter measured 44 "version drift" events over five days, with a mechanism where **every step works as designed yet the instance can never be replaced**: upgrade → the port still serves the previous generation → the host detects drift → it spawns a `--replace` helper → the helper pre-flights and finds `lock.writable=false` → it **correctly refuses** (the 0.10.29 design, zero damage) → the old instance survives → the next boot reports it again. Fixes: ① new `lib/resident-state.js` **records "cannot replace (environment limit)" as state** (cause / lock path / port / versions / count, valid for 7 days); ② the host now **probes lock writability before restarting** and, when unwritable, **stops retrying a doomed replacement**, recording just one line — `warn` the first time, then downgraded to a `boot`-level "known state" on later boots (the reporter's words: they do not want a warn on every boot forever); ③ the wording is explicitly split from "drift": **"the resident instance cannot be replaced (environment limit)"** plus the way out (fix permissions then restart, or run `dsh-memory watchdog --port N` in the foreground); ④ the panel, `/version-check` and `align` all surface the state (`resident.replaceBlocked`; in `align` it is a `warn` finding that clears itself once the environment is fixed). `npm test` now runs **359 checks** |
|
|
365
|
+
| **v0.10.31** | 2026-10-15 | **Root fix for `restart` failing 100% of the time on machines with slow port enumeration (issue #30, third/fourth retest rounds — the reporter traced it to the line)**. Mechanism: the `--replace` helper ran "clear the scene → only then write the lock if the port is free → start the watchdog", while `Get-NetTCPConnection` inside the clear step measured **4–13.6 seconds** and was **not bounded by the 8-second budget** → the helper **never reached the lock write** → the lock stayed empty → `waitForTakeover` (which requires a fresh live watchdog in the lock) **was guaranteed to time out** → on such machines both the CLI and the panel button report "(no response)" **100% of the time, regardless of version**. Fixes: ① **a failed clear step still leaves a registration** (`acquireWatchdogLock({force:true})`), so "a web on the port with an empty lock" can no longer happen; ② **`acquireWatchdogLock` gains `force`** (the whole point of `--replace` is to replace that same-port instance, so it must not yield — otherwise the helper exits before the clear step finishes); ③ 0.10.30 already bounded the enumerator (`defaultExec` kills the child on timeout, `findPortListener({timeoutMs})` threads it through, the clear step uses ≤2.5s); ④ the CLI's `restart` result is now **actually written to the auto-capture log** (the hint always claimed so, but the CLI path never did). They also measured "two competing takeover helpers" (the host tick and the CLI each spawning one) — with registration plus force, that race is far less harmful (the later one takes over the registration instead of both leaving nothing). `npm test` now runs **355 checks** |
|
|
366
366
|
|
|
367
367
|
<details>
|
|
368
|
-
<summary>Older release notes (v0.10.
|
|
368
|
+
<summary>Older release notes (v0.10.30 and earlier · 57 releases, click to expand)</summary>
|
|
369
369
|
|
|
370
370
|
| Version | Date | Highlights |
|
|
371
371
|
|---|---|---|
|
|
372
|
+
| **v0.10.30** | 2026-10-15 | **Two fixes for "the whole page is unusable": malformed tags causing a black screen (#41) plus two real defects in the takeover path (issue #30, third/fourth retest rounds)**. ① **#41**: a string `tags` value sent to `/write` (e.g. `"[\"a\",\"b\"]"`) was passed straight into `writeCard`'s `JSON.stringify` → **double-encoded** in the database → read back as a string → the client's `card.tags.slice(...).map` threw → **the React root failed to mount and the whole page went black** (restarting DSH, the plugin or the resident instance changed nothing, because the bad data lives in the DB). `normalizeTags()` is now the **single entry point**: arrays, JSON strings, comma strings and **legacy double-encoded rows** all collapse to arrays, used by the write path **and every read path** (existing bad data needs no manual repair); the render layer also guards itself (bad tags cost a few chips, never the tree). ② **#30**: the reporter found two more issues inside my 0.10.29 — `restoreWatchdogLock`'s "live registration" test **missed `!w.endedAt`** (so with a reused pid the write-back was abandoned), and **Windows port enumeration had no timeout** (`Get-NetTCPConnection` measured at 4–7.5s, once 13.6s, while the takeover's 8s budget could not bound it → decisions were made on stale data). `defaultExec` now supports a timeout and actually kills the child, `findPortListener({timeoutMs})` threads it through, and the takeover passes a bounded value (≤2.5s). Adds `tests/tags-normalize.test.mjs` (4 checks) and 2 regressions in `watchdog-restart`. `npm test` now runs **354 checks** |
|
|
373
|
+
| **v0.10.29** | 2026-10-14 | **A failed takeover no longer wrecks the crime scene (key finding from the issue #30 retest)**. The reporter measured on 0.10.26 that after a failed `restart` the **lock file was deleted** while the same web kept listening — the scene then looked as if a resident had never existed, leaving only an orphan. Four fixes: ① **releasing the lock no longer deletes the file** (`releaseWatchdogLock` leaves an "ended" record with `endedAt`; `status` shows "exited (stale record)", and such slots **never block** a new instance, even with pid reuse); ② **a failed restart writes the pre-replacement registration back** (`restoreWatchdogLock`, only when no live registration exists for that port); ③ **failing to enumerate the occupant no longer counts as "port free"** — `clearResidentForTakeover` now **double-checks with TCP** (in restricted environments the process query returns nothing, and the old code concluded the port was free, so the new web could not bind, the self-check reported "(no response)" and the lock was wiped); if something is still listening the takeover aborts and **touches no registration at all**; ④ **refuse to take over when the lock file is unwritable** (an unregisterable instance would only become another orphan — it now errors out and points at running the watchdog in the foreground). The restart failure message also names **who is on the port now** and whether the lock was written back, and `status` now **labels the port probe and the lock registration as separate sections** (the reporter pointed out the old output mixed the two and misled readers). `npm test` now runs **348 checks** |
|
|
374
|
+
| **v0.10.28** | 2026-10-14 | **New "version alignment check": see at a glance what stays stale after a plugin upgrade**. Upgrading only replaces **files on disk**, and two kinds of "old" do not go away by themselves: ① **runtime** — the event subscriptions the DSH host registered in memory (`agent/turn-stopping`, `agent/inbox/claimed`, …) come from **the code it loaded at startup**, so without restarting DSH it keeps using the old interfaces (that "turn-stop event appears broken" alert is almost certainly this: measured here, host **v0.10.8** against disk v0.10.27 — and the alert text itself, with its cumulative "135/75", proves the running code predates 0.10.20); ② **persisted wiring** — the MCP and hook entries the plugin writes into `~/.claude.json`, `~/.claude/settings.json`, `~/.codex/config.toml` and `~/.cursor/mcp.json` carry **old install paths, old node paths and old arguments** that an upgrade never rewrites. Adds `lib/align.js`, `GET /align`, `dsh-memory align [--json] [--fix]` and a panel card (usage/today → 🔧 version alignment check) that lists each finding with an **actionable fix** (`fully quit and reopen DSH` / `dsh-memory restart --port N` / `dsh-memory setup` to rewrite the wiring / update the plugin); `--fix` rewrites only the plugin's own keys (your data and config are untouched). Adds `tests/align.test.mjs` (6 checks, including a wording guard for "same node but stale entry"). `npm test` now runs **345 checks** |
|
|
372
375
|
| **v0.10.27** | 2026-10-13 | **"Rebuild from backup" goes from guidance to something you can run (the practical way out for issue #31)**. Once a WAL header is corrupt, code cannot repair the **data** — the previous version could only tell you which backups exist and in what order to swap files; now `dsh-memory doctor --restore latest\|<name>` does it: **without `--yes` it is a dry run** (not a single file touched, and it tells you how many cards the backup holds and whether it would overwrite a writable database). Execution passes four gates: ① **validate the backup** (open read-only + `PRAGMA quick_check` + count cards); ② require an explicit `--force` when the current database is healthy **right now** (decided by a live probe, not a cached health verdict); ③ **abort** if a resident watchdog is still alive (no swapping while writers are active, unless `--force`); ④ **re-verify** afterwards (reopen + write probe + card count). The original scene is **always renamed to `*.corrupt-<timestamp>` and never deleted** (the DB plus `-wal` and `-shm`). Adds `tests/db-restore.test.mjs` (4 checks: rebuild from a broken DB / healthy DB needs force and a dry run touches nothing / dangerous sources refused / health verdict refreshed). `npm test` now runs **339 checks** |
|
|
373
376
|
| **v0.10.26** | 2026-10-13 | **Lock contention now retries asynchronously at the application level instead of using `busy_timeout` (the measured remedy for issues #31/#33)**. Local controlled experiment: while an external process held a write transaction we immediately got `database is locked`, and it **stayed locked even after that process was killed** — the old code had no waiting at all, so a single collision failed permanently (on restricted or externally-occupied drives like A: this shows up as "the whole memory database is broken"). The review rejected `PRAGMA busy_timeout` (`DatabaseSync` from `node:sqlite` is a **synchronous API**, so busy-waiting blocks the host event loop and freezes all of DSH), so we added `withDbRetry()` instead: on a lock, **yield the event loop, sleep briefly and retry** (linear backoff, up to 3 attempts), only on **write routes**; once headers have been sent retrying is explicitly forbidden (`e.noRetry`, preventing double writes), and non-lock errors (data/syntax) are tried once and rethrown unchanged. Tests cover: retry until success / one attempt for non-busy errors / `noRetry` honoured / exhaustion / a source guard against sneaking `busy_timeout` back in. `npm test` now runs **335 checks** |
|
|
374
377
|
| **v0.10.25** | 2026-10-13 | **"Cannot be determined" no longer masquerades as "unmanaged", plus a unified UX for an unwritable DB (closing layer ③ of issue #33)**. ① **Managed state becomes three-valued**: `/version-check` now also reports `resident.managedKnown`, `lock:{path,writable,error}` and `pid` — in restricted environments (sandbox write bans, ACLs, non-system drives) where the watchdog lock file cannot be written, the panel no longer says "unmanaged" (which made users think the instance was broken and restart it repeatedly) but "**management state unknown**: the lock file cannot be written, so it cannot be determined; keep-alive usually still works", together with the real cause and path; `writeWatchdogLock` failures are no longer fully silent either (one traceable line per process). ② **Unified UX when the DB is unwritable**: write routes (delete / merge / card / refine-apply / optimize-execute / …) now **fail fast at the entrance** (503 + `DB_UNAVAILABLE` + recovery guidance), and any write path not enumerated is caught by the outermost wrapper translating raw SQLite text into the same reply; the panel shows a **cross-tab banner** at the top: "the memory database is currently not writable — card reads/writes will fail, but **settings can still be saved**". ③ `dsh-memory doctor` now reports watchdog lock writability (visible at a glance in restricted environments). `npm test` now runs **334 checks** |
|
package/README.md
CHANGED
|
@@ -393,14 +393,17 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
393
393
|
|
|
394
394
|
| 版本 | 日期 | 关键改动 |
|
|
395
395
|
|---|---|---|
|
|
396
|
-
| **v0.10.
|
|
397
|
-
| **v0.10.
|
|
396
|
+
| **v0.10.32** | 2026-10-15 | **「常驻实例不可替换」变成长期状态,而不是每次开机重试(#30 第五轮复测:A: 盘的稳态)**。报告者实测 5 天 44 次「版本漂移」,机制是**每一步都按设计工作、合起来永远换不掉**:升级 → 端口上仍是上一代 → 宿主探测漂移 → 拉起 `--replace` 助手 → 助手前置探到 `lock.writable=false` → **正确地拒绝**(0.10.29 的设计,零破坏)→ 旧实例继续存活 → 下次开机再报一次。修法:① 新增 `lib/resident-state.js`:把「不可替换(环境限制)」**记成状态**(原因 / 锁路径 / 端口 / 版本 / 次数,7 天有效);② 宿主在重启前**先探锁可写性**,不可写就**不再重试注定失败的替换**,只记一条 —— **首次 `warn`**、之后每次开机降级为 `boot` 级「已知状态」(报告者原话:不想每次开机都看到一条 warn 永久持续);③ 措辞与「漂移」显式分开:**「常驻实例不可替换(环境限制)」** + 出路(修权限后 restart,或 `dsh-memory watchdog --port N` 前台观察);④ 面板 / `/version-check` / `align` 都能看到这个状态(`resident.replaceBlocked`;`align` 里是一条 `warn` 级结论,环境修好后自动清除)。`npm test` 共 **359 项** |
|
|
397
|
+
| **v0.10.31** | 2026-10-15 | **`restart` 在「端口枚举很慢」的机器上 100% 失败的根因修复(#30 第三/四轮复测,报告者定位到行)**。机制:`--replace` 助手的顺序是「清场 → 端口空了才写锁 → 启动看门狗」,而清场里 `Get-NetTCPConnection` 实测要 **4–13.6 秒**且不受 8 秒预算约束 → 助手**永远走不到写锁那一步** → 锁里什么都没有 → `waitForTakeover`(要求「锁里出现新的活看门狗」)**必然超时** → CLI 与面板按钮在这类机器上 100% 报「(无响应)」,**与版本号无关**。修法:① **清场失败也必须留下登记**(`acquireWatchdogLock({force:true})`),现场不再出现「端口有 web、锁里空白」;② **`acquireWatchdogLock` 支持 `force`**(`--replace` 的语义就是替换同端口那个,不许让位 —— 否则助手在清场没跑完时先退出);③ 0.10.30 已给枚举执行器加超时(`defaultExec` 真掐断子进程、`findPortListener({timeoutMs})` 逐层传递、清场给 ≤2.5s 有界值);④ CLI 的 `restart` 结果**真的写进自动沉淀日志**(提示里一直这么说,但 CLI 路径过去不写)。他还实测到「两个接管助手互相赛跑」(宿主 tick + CLI 各拉一个)—— 登记 + force 之后这种赛跑的危害大幅降低(后到者接管登记,而不是双双留空)。`npm test` 共 **355 项** |
|
|
398
398
|
|
|
399
399
|
<details>
|
|
400
|
-
<summary>更早的更新日志(v0.10.
|
|
400
|
+
<summary>更早的更新日志(v0.10.30 及以前 · 共 57 版,点开查看)</summary>
|
|
401
401
|
|
|
402
402
|
| 版本 | 日期 | 关键改动 |
|
|
403
403
|
|---|---|---|
|
|
404
|
+
| **v0.10.30** | 2026-10-15 | **两处「让整页不可用」的修复:畸形 tags 黑屏(#41)+ 接管链路的两个真病灶(#30 第三/四轮复测)**。① **#41**:`/write` 传字符串 `tags`(如 `"[\"a\",\"b\"]"`)被 `writeCard` 直接 `JSON.stringify` → **双重编码**落库 → 读出来仍是字符串 → 前端 `card.tags.slice(...).map` 抛错 → **React 根组件挂载失败、整页黑屏**(重启 DSH / 插件 / 常驻实例都无效,因为坏数据一直在库里)。现在 `normalizeTags()` 是**唯一入口**:数组 / JSON 字符串 / 逗号串 / **双重编码的旧数据**都收敛成数组,写路径与**所有读路径**都走它(既有坏数据不必用户手工修);渲染层再加兜底(坏 tags 最多少显示几个标签,不许把整棵树带走)。② **#30**:报告者在我 0.10.29 里又抓出两处 —— `restoreWatchdogLock` 的「活登记」判定**少了 `!w.endedAt`**(PID 复用/正在退出时,写回会被放弃);**Windows 端口枚举没有超时**(实测 `Get-NetTCPConnection` 无条件要 4–7.5 秒、一次 13.6 秒,而接管清场的 8 秒预算根本管不住它 → 决策基于过期数据)。现在 `defaultExec` 支持超时并真的掐断子进程、`findPortListener({timeoutMs})` 逐层传下去、接管清场给有界值(≤2.5s)。新增 `tests/tags-normalize.test.mjs`(4 项)+ `watchdog-restart` 2 项回归。`npm test` 共 **354 项** |
|
|
405
|
+
| **v0.10.29** | 2026-10-14 | **失败的接管不再破坏现场(issue #30 复测的关键发现)**。报告者在 0.10.26 上实测:`restart` 失败后**锁文件被删除**、端口上还是同一个 web —— 现场从此看起来像「从来没有过常驻实例」,只剩一个孤儿。四处修复:① **释放锁不再删文件**(`releaseWatchdogLock` 留一条带 `endedAt` 的「已结束」记录,`status` 会显示「已退出(陈旧记录)」;这种槽**永不阻挡**新实例,pid 复用也不怕);② **失败的 restart 把替换前的登记写回去**(`restoreWatchdogLock`,只在本端口没有活着的登记时才写);③ **枚举不到占用者时不再误判「端口已空」**——`clearResidentForTakeover` 用 **TCP 复核**(受限环境里进程查询被拒会返回空,旧实现据此认为端口空出 → 新 web 绑不上 → 自检「(无响应)」+ 锁被清空),还在监听就中止接管且**一个登记都不动**;④ **锁写不进去时前置拒绝接管**(写不进锁 → 新实例无法登记 → 只会留孤儿;直接报错并给前台观察的出路)。另外 `restart` 失败信息补上「**端口上现在是谁**」+「锁有没有写回」,`status` 把「端口探测」与「锁登记」**分段标注**(报告者指出旧输出两者并列会误导)。`npm test` 共 **348 项** |
|
|
406
|
+
| **v0.10.28** | 2026-10-14 | **新增「版本对齐检查」:把「插件迭代后旧接口 / 旧接线还在」一次查清**。插件升级只换**磁盘文件**,两类「旧」不会自己消失:① **运行时** —— DSH 宿主进程内存里注册的事件订阅(`agent/turn-stopping`、`agent/inbox/claimed`…)来自它**启动时加载的那份代码**,不重启 DSH 就会一直用旧接口(那条「轮次收尾事件疑似失效」多半就是它:本机实测宿主 **v0.10.8** / 磁盘 v0.10.27,而且该告警文案里的「累计 135/75」本身就证明跑的是 ≥0.10.20 之前的旧判据);② **持久接线** —— 插件写进 `~/.claude.json`、`~/.claude/settings.json`、`~/.codex/config.toml`、`~/.cursor/mcp.json` 的 MCP 与 hooks 条目含**旧安装路径 / 旧 node 路径 / 旧参数**,升级后不会自动改写。新增 `lib/align.js` + `GET /align` + `dsh-memory align [--json] [--fix]` + 面板「用量/今日 → 🔧 版本对齐检查」:逐条给结论与**可执行动作**(`完全退出并重开 DSH` / `dsh-memory restart --port N` / `dsh-memory setup` 重写接线 / 更新插件),`--fix` 只重写插件自己的键(不动数据与配置)。新增 `tests/align.test.mjs`(6 项,含「同一 node 但条目是旧写法」的措辞准确性守卫)。`npm test` 共 **345 项** |
|
|
404
407
|
| **v0.10.27** | 2026-10-13 | **「损坏时从备份重建」从引导升级成可执行(issue #31 的现实出路)**。WAL 头一旦损坏,代码修不了**数据** —— 上一版只能告诉你「有哪些备份、按什么顺序替换」,现在多了 `dsh-memory doctor --restore latest\|备份名`:**不带 `--yes` 只演练**(一个文件都不动,先告诉你备份里有几张卡、会不会覆盖可写库);执行时四道闸门:① **校验备份**(只读打开 + `PRAGMA quick_check` + 数卡片);② 当前库**此刻**完好时要求显式 `--force`(用**当场探测**判,不信缓存的体检结论);③ 检测到常驻看门狗还活着就**中止**(避免边写边换,除非 `--force`);④ 恢复后**复核**(重新打开 + 写探针 + 卡片数)。原现场**一律改名 `*.corrupt-<时间戳>` 留证,绝不删除**(库 + `-wal` + `-shm` 一起)。新增 `tests/db-restore.test.mjs`(4 项:坏库重建 / 完好库需 force 且演练不动任何文件 / 拒绝危险来源 / 体检结论刷新)。`npm test` 共 **339 项** |
|
|
405
408
|
| **v0.10.26** | 2026-10-13 | **撞锁改用「应用层异步重试」,而不是 `busy_timeout`(issue #31/#33 实测对症)**。本机对照实验:外部进程持写事务时我方立刻拿到 `database is locked`,而且**对方被强杀之后仍然锁着** —— 老实现没有忙等,一撞锁就永久失败(在 A: 盘那类受限 / 被外部占用的现场,表现出来就是「整个记忆库坏了」)。评审已否决 `PRAGMA busy_timeout`(`node:sqlite` 的 `DatabaseSync` 是**同步 API**,忙等会阻塞宿主事件循环、把整个 DSH 卡住),所以改成新增 `withDbRetry()`:撞锁时**让出事件循环睡一小会儿再试**(线性退避,最多 3 次),只用**写路由**;「响应已发出」时显式禁止重试(`e.noRetry`,避免重复写),非锁类错误(数据 / 语法)只试一次并原样抛。测试覆盖:重试到成功 / 非忙错误只试一次 / `noRetry` 生效 / 试满仍失败 / 源码守卫(不许偷偷加 `busy_timeout`)。`npm test` 共 **335 项** |
|
|
406
409
|
| **v0.10.25** | 2026-10-13 | **「判不出来」不再冒充「未托管」+ 库不可写时的统一 UX(issue #33 第③层收口)**。① **托管状态变三态**:`/version-check` 增报 `resident.managedKnown`、`lock:{path,writable,error}` 与 `pid` —— 受限环境(沙箱禁写 / ACL / 非系统盘)写不进看门狗锁文件时,面板不再显示「未托管」(那会让用户以为实例坏了、反复重启),改为「**托管状态未知**:锁文件写不进去 → 无法判断;保活通常仍然有效」,并同时显示真实原因与路径;`writeWatchdogLock` 的失败也不再全静默(每个进程留一条可查的痕)。② **库不可写时统一 UX**:写路由(delete / merge / card / refine-apply / optimize-execute / …)在入口**短路径失败**(503 + `DB_UNAVAILABLE` + 恢复指引),没枚举到的写路径由最外层兜底把 SQLite 原文翻译成同一套答复;面板**跨标签顶部**明示「记忆库当前不可写 —— 卡片读写会失败,但**配置仍可保存**」。③ `dsh-memory doctor` 增报看门狗锁可写性(受限环境一眼可见)。`npm test` 共 **334 项** |
|
package/bin/dsh-memory.mjs
CHANGED
|
@@ -376,11 +376,21 @@ async function main() {
|
|
|
376
376
|
const out = await restartResident({ port, vaultRoot, interval, maxRestart })
|
|
377
377
|
if (!out.ok) {
|
|
378
378
|
console.error(`替换失败(${out.stage}):${out.error || '未知原因'}`)
|
|
379
|
+
// issue #30:提示里写着「接管结论也会写进自动沉淀日志」,但 CLI 这条路径过去**不写** ——
|
|
380
|
+
// 用户按提示去「设置 → 记忆 → 用量/今日」里什么也找不到。这里补齐(与宿主侧同一份日志)。
|
|
381
|
+
try {
|
|
382
|
+
const { appendCaptureLog } = await import('../lib/capture-log.js')
|
|
383
|
+
await appendCaptureLog({ time: Date.now(), sessionId: 'system', action: 'warn', reason: `CLI 替换常驻实例失败(${out.stage}):${out.error || '未知原因'}` })
|
|
384
|
+
} catch { /* 日志写不了不影响主流程 */ }
|
|
379
385
|
console.error(`排查:dsh-memory status --port ${port};若端口被别的进程占着,先结束它再 restart;`)
|
|
380
386
|
console.error(' 接管结论也会写进「自动沉淀日志」(设置 → 记忆 → 用量/今日)里。')
|
|
381
387
|
process.exitCode = 1
|
|
382
388
|
return
|
|
383
389
|
}
|
|
390
|
+
try {
|
|
391
|
+
const { appendCaptureLog } = await import('../lib/capture-log.js')
|
|
392
|
+
await appendCaptureLog({ time: Date.now(), sessionId: 'system', action: 'boot', reason: `CLI 替换常驻实例成功:端口 ${port} 现在服务 v${out.served}(等待 ${Math.round(out.waitedMs / 1000)}s)` })
|
|
393
|
+
} catch { /* 日志写不了不影响主流程 */ }
|
|
384
394
|
console.log(`已在后台启动新看门狗 pid=${out.helperPid} port=${port} vault=${vaultRoot}`)
|
|
385
395
|
console.log(`自检通过:端口 ${port} 正在服务 v${out.served}(等待 ${Math.round(out.waitedMs / 1000)}s)`)
|
|
386
396
|
return
|
package/index.js
CHANGED
|
@@ -1359,6 +1359,29 @@ export function apply(ctx, config) {
|
|
|
1359
1359
|
}
|
|
1360
1360
|
|
|
1361
1361
|
if (decision.action === 'restart') {
|
|
1362
|
+
// issue #30(第五轮复测的稳态):**先看这个环境到底能不能替换**。锁目录不可写时,
|
|
1363
|
+
// 助手会在前置检查里正确地拒绝 —— 那就别再每次开机当成「漂移」重试一遍了:
|
|
1364
|
+
// 把「不可替换」记成状态、只提示出路(首次 warn,之后降级为一条已知状态)。
|
|
1365
|
+
let lockWritable = true
|
|
1366
|
+
try { lockWritable = Boolean(m.probeLockWritable().writable) } catch { lockWritable = true }
|
|
1367
|
+
if (!lockWritable) {
|
|
1368
|
+
let blocked = null
|
|
1369
|
+
try {
|
|
1370
|
+
const st = await import('./lib/resident-state.js')
|
|
1371
|
+
const info = m.probeLockWritable()
|
|
1372
|
+
const rec = st.recordReplaceBlocked(process.env, { error: info.error, lockPath: info.path, port: wdPort, version: cur, served: info.served || servedLabel })
|
|
1373
|
+
blocked = st.replaceBlockedInfo(process.env)
|
|
1374
|
+
const line = `常驻实例不可替换(环境限制):看门狗锁目录不可写(${info.error || '未知原因'};${info.path})→ 端口 ${wdPort} 上会长期停在 v${servedLabel},本机已是 v${cur || '?'}。出路:修复该目录权限后执行 dsh-memory restart --port ${wdPort},或前台观察 dsh-memory watchdog --port ${wdPort}`
|
|
1375
|
+
console.error(`[memory-eternal] ${line}`)
|
|
1376
|
+
// 首次(或状态过期)用 warn 让用户注意到;之后每次开机只记一条 boot 级「已知状态」,
|
|
1377
|
+
// 不再显示成失败 —— 报告者的原话:这台机器「每次开机一条 warn,永久持续」。
|
|
1378
|
+
logCapture('system', rec.firstTime ? 'warn' : 'boot', line)
|
|
1379
|
+
} catch { /* 状态记不下来也不影响:至少不误重试 */ }
|
|
1380
|
+
void blocked
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
// 环境能写 → 清掉历史状态(修好权限后不该再显示「不可替换」)
|
|
1384
|
+
try { const st = await import('./lib/resident-state.js'); st.clearReplaceBlocked(process.env) } catch { /* 无妨 */ }
|
|
1362
1385
|
// 「更新运行中的程序」的唯一安全形态 = 用新代码重启那个常驻进程(不是热替换内存里的模块)
|
|
1363
1386
|
const why = `版本漂移:端口 ${wdPort} 上服务 v${servedLabel} ≠ 本机 v${cur}(${decision.reason})→ 用新代码重启常驻实例`
|
|
1364
1387
|
console.error(`[memory-eternal] ${why}`)
|
package/lib/align.js
CHANGED
|
@@ -22,7 +22,7 @@ import { probeServedVersion } from './watchdog.js'
|
|
|
22
22
|
* @returns {{ok:boolean, findings:Array<{id:string,level:'ok'|'warn'|'fix',title:string,detail:string,command:string}>, counts:{ok:number,warn:number,fix:number}}}
|
|
23
23
|
*/
|
|
24
24
|
export function alignFindings({
|
|
25
|
-
hostVersion = '', onDisk = '', residentServed = '', residentPort = 7999,
|
|
25
|
+
hostVersion = '', onDisk = '', residentServed = '', residentPort = 7999, replaceBlocked = null,
|
|
26
26
|
latest = '', updateCommand = '', wiring = [], hookPaths = [], configMirror = null, knownKeys = null,
|
|
27
27
|
} = {}) {
|
|
28
28
|
const findings = []
|
|
@@ -46,6 +46,13 @@ export function alignFindings({
|
|
|
46
46
|
push('resident-ok', 'ok', `常驻实例 v${residentServed} = 磁盘 v${onDisk}`)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// ②b 「不可替换(环境限制)」:这类机器上常驻实例**永远追不平**,必须显式说明,
|
|
50
|
+
// 而不是让用户以为它只是「暂时落后」(issue #30 第五轮复测的原话)。
|
|
51
|
+
if (replaceBlocked && replaceBlocked.fresh) {
|
|
52
|
+
push('resident-replace-blocked', 'warn', '常驻实例不可替换(环境限制:看门狗锁目录不可写)',
|
|
53
|
+
'锁目录当前不可写(' + (replaceBlocked.error || '原因未知') + ';' + (replaceBlocked.lockPath || '') + ')→ 端口 ' + residentPort + ' 上会长期停在 v' + (replaceBlocked.served || replaceBlocked.version || '?') + ',自动替换被正确地拒绝(避免留下孤儿)。',
|
|
54
|
+
'修复该目录权限后执行 dsh-memory restart --port ' + residentPort + ';或前台观察:dsh-memory watchdog --port ' + residentPort)
|
|
55
|
+
}
|
|
49
56
|
// ③ npm 上是否已有更新
|
|
50
57
|
if (latest && onDisk && latest !== onDisk) {
|
|
51
58
|
push('update-available', 'warn', `npm 上已有 v${latest}(本机 v${onDisk})`,
|
|
@@ -176,6 +183,8 @@ export async function inspectAlignment({ hostVersion = '', residentPort = 7999,
|
|
|
176
183
|
}
|
|
177
184
|
} catch { configMirror = { exists: false, path: '', keys: [], unknownKeys: [] } }
|
|
178
185
|
|
|
179
|
-
|
|
186
|
+
let replaceBlocked = null
|
|
187
|
+
try { const st = await import('./resident-state.js'); replaceBlocked = st.replaceBlockedInfo(process.env) } catch { replaceBlocked = null }
|
|
188
|
+
const report = alignFindings({ hostVersion, onDisk, residentServed, residentPort, replaceBlocked, latest, updateCommand, wiring, hookPaths, configMirror, knownKeys })
|
|
180
189
|
return { ...report, onDisk, hostVersion, residentServed, latest, wiring, hookPaths, configMirror }
|
|
181
190
|
}
|
package/lib/api.js
CHANGED
|
@@ -328,6 +328,10 @@ export function createApi(deps = {}) {
|
|
|
328
328
|
// 用户会以为实例坏了、反复重启。这里把「判不出来」单独标出来,并解释原因。
|
|
329
329
|
const lockInfo = probeLockWritable(process.env)
|
|
330
330
|
const managedKnown = residentManaged || lockInfo.writable
|
|
331
|
+
// 「不可替换(环境限制)」的长期状态(issue #30 第五轮复测):面板据此显示成**环境限制**,
|
|
332
|
+
// 而不是每次开机都当成「漂移 → 替换失败」。
|
|
333
|
+
let replaceBlocked = null
|
|
334
|
+
try { const st = await import('./resident-state.js'); replaceBlocked = st.replaceBlockedInfo(process.env) } catch { replaceBlocked = null }
|
|
331
335
|
json(res, 200, {
|
|
332
336
|
ok: true,
|
|
333
337
|
loaded,
|
|
@@ -341,6 +345,7 @@ export function createApi(deps = {}) {
|
|
|
341
345
|
managedKnown,
|
|
342
346
|
pid: process.pid,
|
|
343
347
|
lock: lockInfo,
|
|
348
|
+
replaceBlocked,
|
|
344
349
|
},
|
|
345
350
|
stale: !!(loaded && onDisk && loaded !== onDisk),
|
|
346
351
|
// 只在 **npm 上确实更新** 时提示升级:本地跑着未发布版本(磁盘 0.10.7 / npm 0.10.6)时
|