memory-eternal 0.10.4 → 0.10.5
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 +1 -0
- package/README.md +1 -0
- package/index.js +19 -1
- package/lib/api.js +30 -6
- package/lib/client.js +11 -11
- package/lib/config-sync.js +61 -1
- package/lib/host-heartbeat.js +72 -0
- package/package.json +2 -2
- package/web/app.js +12 -12
|
@@ -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.5",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.en.md
CHANGED
|
@@ -327,6 +327,7 @@ Diagnostics:
|
|
|
327
327
|
|
|
328
328
|
| Version | Date | Highlights |
|
|
329
329
|
|---|---|---|
|
|
330
|
+
| **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** |
|
|
330
331
|
| **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** |
|
|
331
332
|
| **v0.10.3** | 2026-10-06 | **Fixed the counting semantics of `audit list` (#20).** ① The total is now **decoupled from `--limit`**: the reported count is the **matching total** (via `countCards`) while `--limit` caps output only, and truncation is stated explicitly (`显示前 M 张 … 还有 X 张未显示`). ② **No more silent truncation by default** (omit `--limit`, or `--limit 0`, to list everything). ③ `--json` now exposes `total` / `returned` / `count` (a backward-compatible alias of total; in 0.10.2 it wrongly equalled returned) / `limit` (null = all) / `hasMore`. ④ The meaning of `--status all` is documented in the usage text and README: the audit queue (pending + rejected), **excluding** dequeued approved and recycle-bin deleted; `approved` remains queryable explicitly. ⑤ **Same defect fixed in the MCP `memory_audit_list` tool**: it also silently capped at 20 and reported the truncated length as the pending count; it now reports the matching total plus how many are hidden, cap 100 → 500. |
|
|
332
333
|
| **v0.10.2** | 2026-10-06 | **Five long-standing issues fixed at once (#15 / #16 / #17 / #18 / #19).** ① **Per-project vaults finally work in the host (#15-1)**: the host matched `vaultProfiles[].match.workspace` against the process cwd (the directory dsh was launched from) — it now uses the **session's own workspace** from `session.header.cwd` (both capture and `memory_recall`), while CLI/MCP/hooks keep their existing cwd behaviour. ② **Distillation stops burning doomed candidates (#15-2)**: providers declaring `supportsReasoningEffort:false` no longer receive a `reasoningEffort` field (unknown = supported), and they move behind supporting candidates instead of being dropped. ③ **Truncated output is no longer misreported as a parse failure (#18)**: a dedicated `MAX_TOKENS` code (with the actual cap in the message), one automatic retry with a doubled cap, failure reporting that **always keeps the first candidate's error as the primary cause** plus a per-code summary of the rest, default `captureMaxTokens` 900 → **2000**, and a `distill-failed` tag on fallback raw cards. ④ **Standalone-web config saves are no longer lost silently (#16)**: drain errors are no longer swallowed (stderr + capture log, throttled), and the shared `memory-eternal-config.json` is now written **atomically (tmp + rename)** — the old non-atomic write could be read mid-truncation by the standalone web / MCP, fail JSON.parse and silently fall back to defaults, the pending file is **kept** with `dropped:true` + `lastError` after retries are exhausted, and the state shows up in `/diagnostics` and `/config`. ⑤ **Audit CLI (#17)**: `dsh-memory audit list/approve/reject` (multiple paths, `--json` for jq) reuses the same `setCardStatus` guard and `audit_log`; MCP gains only a **read-only** `memory_audit_list`. ⑥ **Watchdog lifecycle (#19)**: new `dsh-memory status/stop/restart`, `pkgVersion` recorded in the lock (status warns when the resident instance is outdated), truthful `spawned` vs `delegated to existing pid` host logs, a clear notice when `watchdogAutoSpawn` is off but an instance is still running, `dsh-memory stop` also stops the **web server the watchdog spawned** (on Windows `process.kill(pid,'SIGTERM')` terminates unconditionally, so the watchdog's own exit handler never runs — the old behaviour left an orphan web holding the port; the web pid is recorded in the lock and confirmed against the process command line before killing, avoiding pid-reuse mistakes), plus README fixes for the contradictory default and the "restart turns it off" promise. |
|
package/README.md
CHANGED
|
@@ -366,6 +366,7 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
366
366
|
|
|
367
367
|
| 版本 | 日期 | 关键改动 |
|
|
368
368
|
|---|---|---|
|
|
369
|
+
| **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 项** |
|
|
369
370
|
| **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 项** |
|
|
370
371
|
| **v0.10.3** | 2026-10-06 | **修复 `audit list` 的计数语义(#20)**。① 总数与 `--limit` **解耦**:`共 N 张` 取**匹配总数**(`countCards`),`--limit` 只限制显示条数,截断时明确写出 `显示前 M 张 … 还有 X 张未显示`。② **默认不再静默截断**(不给 `--limit`、或 `--limit 0` = 全部显示)。③ `--json` 拆分为 `total`(匹配总数)/ `returned`(本次返回)/ `count`(= total 的兼容别名;0.10.2 里它误等于 returned)/ `limit`(null = 全部)/ `hasMore`。④ `--status all` 的语义写进用法与 README:= 审核队列(pending + rejected),**不含**已出队的 approved 与回收中心的 deleted;`approved` 可显式查询。⑤ **同源修复 MCP 的 `memory_audit_list`**:它同样存在「默认 20 静默截断 + 把截断条数报成待处理数」,现改为报匹配总数 + 未显示提示,上限 100 → 500。 |
|
|
371
372
|
| **v0.10.2** | 2026-10-06 | **五个存量 issue 集中修复(#15 / #16 / #17 / #18 / #19)**。① **按项目选库在宿主侧终于生效(#15-1)**:宿主过去拿进程 cwd 去匹配 `vaultProfiles[].match.workspace`,而那是「启动 dsh 的目录」——现在从 `session.header.cwd` 取**会话自己的工作区**(capture 与 memory_recall 两条路径都传),CLI/MCP/hooks 行为不变。② **蒸馏不再选错 provider(#15-2)**:声明 `supportsReasoningEffort:false` 的候选不再被传 `reasoningEffort`(能力未知一律按支持处理),并把它们排到支持者之后但**不删除**,避免兜底全无。③ **输出截断不再伪装成解析失败(#18)**:新增独立错误码 `MAX_TOKENS`(含实际上限值),撞上限自动以双倍上限重试同一候选;兜底上报改为**始终以第一候选为主因** + 其余错误按码汇总(不再被最后一个候选覆盖);默认 `captureMaxTokens` 900 → **2000**;失败兜底的原文卡加 `distill-failed` 标签便于过滤。④ **独立 Web 端保存配置不再静默丢失(#16)**:drain 报错不再被 `catch {}` 吞掉(进 stderr + 自动沉淀日志,5 分钟去重节流),共享配置 `memory-eternal-config.json` 改为 **tmp + rename 原子写**(原先非原子写会在截断窗口内被独立 web / MCP 读到半截内容 → JSON.parse 失败 → 设置静默回落成默认值),重试耗尽后**保留 pending 文件**并标注 `dropped:true` + `lastError`(不再删除用户改动),失败状态进 `/diagnostics` 与 `/config` 响应。⑤ **审核中心有了 CLI(#17)**:`dsh-memory audit list/approve/reject`(多路径批量、`--json` 便于 jq 筛选、复用同一套 `setCardStatus` 守卫与 `audit_log`);MCP 侧只新增**只读**的 `memory_audit_list`,不暴露 approve/reject。⑥ **看门狗有了生命周期(#19)**:新增 `dsh-memory status/stop/restart`,锁文件写入 `pkgVersion`(status 会提示「常驻实例是旧版」),宿主日志按抢锁结果如实打印 `spawned` / `delegated to existing pid`(不再每次都谎报 spawned),关闭 `watchdogAutoSpawn` 时明确提示既有实例仍在跑;并且 `dsh-memory stop` 会**连同该 watchdog 拉起的 web 一起停**(Windows 上 `process.kill(pid,'SIGTERM')` 是无条件终止,watchdog 的退出处理器根本不会执行 —— 旧写法会在 stop 后留下一个占着端口的孤儿 web;web 子进程 pid 现在记在锁里,杀之前还会用进程命令行确认确实是 `web.js`,防 pid 复用误杀);README 修正「默认值自相矛盾」与「重启即可关闭」的错误承诺。 |
|
package/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { summarizeTurn, summarizeTurnDetailed, routeCandidates, extractLastTurn,
|
|
|
34
34
|
import { createApi, json, encodeBody } from './lib/api.js'
|
|
35
35
|
import { appendCaptureLog, readCaptureLog, rotateCaptureLog } from './lib/capture-log.js'
|
|
36
36
|
import { missingWebAssets, missingAssetsReason } from './lib/web-assets.js'
|
|
37
|
+
import { writeHostMarker, clearHostMarker } from './lib/host-heartbeat.js'
|
|
37
38
|
import { nodeBinary, childEnv } from './lib/node-bin.js'
|
|
38
39
|
|
|
39
40
|
export const name = 'memory-eternal'
|
|
@@ -405,14 +406,31 @@ export function apply(ctx, config) {
|
|
|
405
406
|
ctx.effect(() => () => clearInterval(pendingTimer), 'memory-eternal: pending config sync')
|
|
406
407
|
ctx.effect(() => {
|
|
407
408
|
let stop = () => {}
|
|
409
|
+
// 动态 import 是异步的:若宿主在它 resolve 之前就 dispose(测试里的假 ctx 会立刻 dispose),
|
|
410
|
+
// 后到的监听器就再也没人清理 —— 会一直挂住事件循环。用一个 disposed 标记堵住这个窗口。
|
|
411
|
+
let disposed = false
|
|
408
412
|
import('./lib/config-sync.js').then(({ watchPendingConfig }) => {
|
|
413
|
+
if (disposed) return
|
|
409
414
|
stop = watchPendingConfig(process.env, () => {
|
|
410
415
|
drainPending().then(() => hub.broadcast('config', { at: Date.now(), source: 'pending' })).catch(() => {})
|
|
411
416
|
})
|
|
412
417
|
}).catch(() => {})
|
|
413
|
-
return () => { try { stop() } catch { /* 已停止 */ } }
|
|
418
|
+
return () => { disposed = true; try { stop() } catch { /* 已停止 */ } }
|
|
414
419
|
}, 'memory-eternal: pending config watch')
|
|
415
420
|
|
|
421
|
+
// 宿主心跳(issue #21):告诉独立 Web 端「本机有 DSH 宿主在跑」,这样它就不会绕过宿主
|
|
422
|
+
// 直接写共享配置(绕过会与宿主的 volatile 配置分叉)。心跳必须定期刷新:进程被硬杀时
|
|
423
|
+
// 没有机会删文件,独立端靠「新鲜度 + pid 存活」两个条件判定,所以宁可多刷几次。
|
|
424
|
+
ctx.effect(() => {
|
|
425
|
+
const beat = () => { try { writeHostMarker(process.env, { version: versionRef }) } catch { /* 心跳失败不影响主流程 */ } }
|
|
426
|
+
beat()
|
|
427
|
+
// unref:心跳只是「我在跑」的旁证,不该成为阻止进程退出的最后一根钉子
|
|
428
|
+
// (测试里以假 ctx 跑 apply() 时会因此挂住不退,实测过一次)。
|
|
429
|
+
const timer = setInterval(beat, 15000)
|
|
430
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
431
|
+
return () => { clearInterval(timer); try { clearHostMarker(process.env) } catch { /* 清理失败无妨 */ } }
|
|
432
|
+
}, 'memory-eternal: host heartbeat')
|
|
433
|
+
|
|
416
434
|
// 所有 profile 目录(当前激活 + 其余命名的),供跨库聚合。
|
|
417
435
|
const vaultRoots = (workspace) => {
|
|
418
436
|
const cfg = settings.get() ?? {}
|
package/lib/api.js
CHANGED
|
@@ -488,7 +488,7 @@ export function createApi(deps = {}) {
|
|
|
488
488
|
}
|
|
489
489
|
if (bad.length) return json(res, 400, { ok: false, error: '字段值不合法:' + bad.join('、'), fields: bad })
|
|
490
490
|
if (!Object.keys(clean).length) return json(res, 400, { ok: false, error: '没有可写入的字段' })
|
|
491
|
-
const { writePendingConfig, readPendingConfig, waitPendingOutcome } = await import('./config-sync.js')
|
|
491
|
+
const { writePendingConfig, readPendingConfig, waitPendingOutcome, clearPendingConfig, applySharedConfigDirect, describeApplyFailure } = await import('./config-sync.js')
|
|
492
492
|
// #16 建议 4:上一份改动是否已被判定失败?是的话这次不能再报「一切正常」——
|
|
493
493
|
// 否则用户第 N 次点击保存,看到的仍是成功,而问题一直在(其实是 settings.update 在报错)。
|
|
494
494
|
const before = readPendingConfig(process.env)
|
|
@@ -497,16 +497,40 @@ export function createApi(deps = {}) {
|
|
|
497
497
|
// issue #21 建议 3:别急着报「一切正常」。宿主在跑时对 pending 文件有 fs.watch
|
|
498
498
|
// (毫秒级),稍等一下回读,就能分清「真的应用了」还是「只是排上队了」——
|
|
499
499
|
// 以前无论哪种都回 ok:true + 一句泛泛的 note,用户只会在几秒后从日志里发现失败。
|
|
500
|
-
|
|
500
|
+
let outcome = await waitPendingOutcome(process.env, { timeoutMs: 800 })
|
|
501
|
+
// issue #21 的宿主无关路径:**本机没有活着的 DSH 宿主**时(Codex / Claude Code /
|
|
502
|
+
// 只跑 dsh-memory serve 的用户),没有任何进程会来 drain pending 文件 —— 那句
|
|
503
|
+
// 「DSH 未运行时下次启动生效」在那类环境下永远不会兑现。这时直接原子写共享配置
|
|
504
|
+
// (hooks / MCP / 独立页都读这个文件),让保存当场生效。
|
|
505
|
+
// 宿主活着时绝不走这条:共享文件是宿主的派生镜像,绕过它直写会被下一次 syncConfigFile 盖掉。
|
|
506
|
+
let appliedDirect = false
|
|
507
|
+
if (outcome === 'queued') {
|
|
508
|
+
const { hostAlive } = await import('./host-heartbeat.js')
|
|
509
|
+
if (!hostAlive(process.env)) {
|
|
510
|
+
try {
|
|
511
|
+
applySharedConfigDirect(process.env, clean)
|
|
512
|
+
clearPendingConfig(process.env)
|
|
513
|
+
outcome = 'applied-direct'
|
|
514
|
+
appliedDirect = true
|
|
515
|
+
} catch { /* 直写失败 → 保持 queued,如实回报,改动仍留在 pending 文件里 */ }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
501
518
|
const stale = before && (before.dropped || before.lastError)
|
|
519
|
+
const advice = describeApplyFailure(before && before.lastError)
|
|
520
|
+
const hostGuard = advice.kind === 'host-guard'
|
|
502
521
|
const note = outcome === 'applied'
|
|
503
522
|
? '宿主已应用该改动。'
|
|
504
|
-
: outcome === '
|
|
505
|
-
? '
|
|
506
|
-
:
|
|
523
|
+
: outcome === 'applied-direct'
|
|
524
|
+
? '本机没有 DSH 宿主在运行,已直接写入共享配置(hooks / MCP / 独立页立即生效)。'
|
|
525
|
+
: outcome === 'failed'
|
|
526
|
+
? '改动已写入,但宿主侧应用失败:' + (hostGuard ? advice.hint : '详见「反馈异常」里的诊断信息。')
|
|
527
|
+
: '已写入待应用:DSH 端会自动同步(DSH 未运行时下次启动生效)'
|
|
507
528
|
return json(res, 200, {
|
|
508
|
-
ok: true, applied: Object.keys(clean), pending: Object.keys(clean), retried: false, at: out.at,
|
|
529
|
+
ok: true, applied: Object.keys(clean), pending: appliedDirect ? [] : Object.keys(clean), retried: false, at: out.at,
|
|
509
530
|
pendingOutcome: outcome,
|
|
531
|
+
appliedDirect,
|
|
532
|
+
hostGuard: hostGuard || undefined,
|
|
533
|
+
hint: hostGuard ? advice.hint : undefined,
|
|
510
534
|
warning: stale
|
|
511
535
|
? '上一次写入待应用时连续失败(' + (before.lastError || '原因未记录') + ');本次已重新写入并重试,如仍不生效请查看「反馈异常」里的诊断信息'
|
|
512
536
|
: undefined,
|