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
package/lib/config-sync.js
CHANGED
|
@@ -98,6 +98,62 @@ export function clearPendingConfig(env = process.env) {
|
|
|
98
98
|
try { fs.unlinkSync(pendingConfigPath(env)); return true } catch { return false }
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* 直接把 patch 合并进**共享配置文件**(宿主无关路径:本机没有 DSH 宿主时用)。
|
|
103
|
+
*
|
|
104
|
+
* 只在「确认没有活着的 DSH 宿主」时调用(见 lib/host-heartbeat.js):宿主活着时共享文件是
|
|
105
|
+
* 它的派生镜像,绕过它直写会与 volatile 配置分叉,下一次 syncConfigFile() 就会盖回来。
|
|
106
|
+
* 合并(而不是整体替换)是有意为之:老版本写下的键、以及其它工具手工加的键都要留住。
|
|
107
|
+
*
|
|
108
|
+
* @returns {object} 合并后的完整配置
|
|
109
|
+
*/
|
|
110
|
+
export function applySharedConfigDirect(env, patch) {
|
|
111
|
+
const file = configFilePath(env)
|
|
112
|
+
let current = {}
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
115
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) current = parsed
|
|
116
|
+
} catch { current = {} }
|
|
117
|
+
const next = { ...current, ...(patch && typeof patch === 'object' ? patch : {}) }
|
|
118
|
+
writeFileAtomicSync(file, JSON.stringify(next, null, 2))
|
|
119
|
+
return next
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 宿主守卫类报错的特征串 —— 同一族缺陷在不同宿主/不同服务上的措辞不同(issue #21):
|
|
123
|
+
// · Cordis HMR 宿主(官方桌面版):runExclusive 拒绝嵌套事务
|
|
124
|
+
// · dsh-tui 一类宿主:能力守卫在 settings.update 内部的 describe() 上抛
|
|
125
|
+
const HOST_GUARD_PATTERNS = [
|
|
126
|
+
/HMR transactions cannot be nested/i,
|
|
127
|
+
/is unavailable from a plugin activation/i,
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
/** 这个报错是不是「宿主不允许插件从自己的回调上下文写配置」。 */
|
|
131
|
+
export function isHostGuardError(message) {
|
|
132
|
+
const s = String(message || '')
|
|
133
|
+
return HOST_GUARD_PATTERNS.some((re) => re.test(s))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 把「应用失败」翻译成给用户看的话(诊断信息 / 自动沉淀日志 / 独立页提示共用)。
|
|
138
|
+
*
|
|
139
|
+
* host-guard 类**不是配置内容的问题**,所以说清「去哪改」比重复报错有用得多:
|
|
140
|
+
* 宿主自己的设置页走的写路径由宿主实现,不受插件上下文限制。
|
|
141
|
+
*
|
|
142
|
+
* @returns {{kind:'host-guard'|'other', hint:string}}
|
|
143
|
+
*/
|
|
144
|
+
export function describeApplyFailure(message) {
|
|
145
|
+
const raw = String(message || '')
|
|
146
|
+
if (!raw) return { kind: 'other', hint: '' }
|
|
147
|
+
if (isHostGuardError(raw)) {
|
|
148
|
+
return {
|
|
149
|
+
kind: 'host-guard',
|
|
150
|
+
hint: '此宿主不允许插件从自己的回调上下文里写配置(原报错:' + raw + ')—— 与配置内容无关。请到「DSH 设置 → 记忆」里改同一项(那条写路径由宿主自己实现,不受此限制),或直接编辑 profile 的配置补丁层。',
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { kind: 'other', hint: '原报错:' + raw + ';可到「DSH 设置 → 记忆」重试,或查看「反馈异常」里的诊断信息。' }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
101
157
|
/**
|
|
102
158
|
* 监听待应用文件的变化(毫秒级触发),用于把「独立页保存 → DSH 应用」从 5 秒轮询
|
|
103
159
|
* 提升到准即时。监听失败(平台/权限)时退化为不监听 —— 仍有 5 秒轮询兜底。
|
|
@@ -224,7 +280,11 @@ export async function drainPendingConfig(env, apply, maxTries = 5) {
|
|
|
224
280
|
const reason = String((error && error.message) || error)
|
|
225
281
|
if (tries >= maxTries) {
|
|
226
282
|
writePendingConfig(env, pending.patch, pending.at, tries, { dropped: true, lastError: reason })
|
|
227
|
-
|
|
283
|
+
// 失败原因分两类:宿主守卫类(不是配置内容的问题,要说清去哪改)与其它。
|
|
284
|
+
const advice = describeApplyFailure(reason)
|
|
285
|
+
const dropped = new Error('待应用配置连续失败 ' + tries + ' 次已放弃(原报错:' + reason + ')'
|
|
286
|
+
+ (advice.kind === 'host-guard' ? '。' + advice.hint : '')
|
|
287
|
+
+ ';改动仍保留在 ' + pendingConfigPath(env) + ',可修正后重新保存')
|
|
228
288
|
dropped.dropped = true
|
|
229
289
|
dropped.lastError = reason
|
|
230
290
|
throw dropped
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// 宿主心跳:让「独立 Web 端」知道本机此刻有没有活着的 DSH 宿主(issue #21 的宿主无关写路径)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它:配置只有一个权威写入者 —— DSH 宿主(插件用 settings.update 应用,再由
|
|
4
|
+
// index.js 的 syncConfigFile 写共享配置文件)。独立 Web 进程里没有 settings 服务,只能把改动
|
|
5
|
+
// 写进「待应用」文件、等宿主来 drain。但**非 DSH 宿主**(Codex / Claude Code / Cursor,
|
|
6
|
+
// 或只跑 `dsh-memory serve` 的用户)根本没有宿主来 drain:改动会永远停在 pending 文件里,
|
|
7
|
+
// 保存等于没保存 —— 而界面上那句「下次启动生效」在那类环境下是错的(不会有"下次启动 DSH")。
|
|
8
|
+
//
|
|
9
|
+
// 判据:宿主活着时周期性刷新 `<共享配置>.host.json`(pid + 版本 + 时间戳)。
|
|
10
|
+
// 独立端保存后若「没人消费 pending」且「心跳不新鲜」,即判定本机没有宿主 → 直接原子写共享配置。
|
|
11
|
+
// 反之(宿主活着)**绝不**绕过宿主写文件:那会与宿主的 volatile 配置分叉,下一次
|
|
12
|
+
// syncConfigFile() 就会把改动盖回去。
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import { configFilePath } from './capture-run.js'
|
|
15
|
+
import { writeFileAtomicSync } from './config-sync.js'
|
|
16
|
+
|
|
17
|
+
/** 心跳文件路径(与共享配置同目录:`xxx.json` → `xxx.host.json`)。 */
|
|
18
|
+
export function hostMarkerPath(env = process.env) {
|
|
19
|
+
return String(configFilePath(env)).replace(/\.json$/i, '.host.json')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 进程存活判定(EPERM = 存在但无权发信号,仍算活着)。 */
|
|
23
|
+
export function isProcessAlive(pid) {
|
|
24
|
+
const n = Number(pid)
|
|
25
|
+
if (!Number.isFinite(n) || n <= 0) return false
|
|
26
|
+
try {
|
|
27
|
+
process.kill(n, 0)
|
|
28
|
+
return true
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return Boolean(error && error.code === 'EPERM')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 写/刷新心跳(原子写:这是跨进程判据,读方可能正好在写)。 */
|
|
35
|
+
export function writeHostMarker(env = process.env, info = {}, now = Date.now()) {
|
|
36
|
+
const data = { pid: process.pid, at: now, ...info }
|
|
37
|
+
writeFileAtomicSync(hostMarkerPath(env), JSON.stringify(data, null, 2))
|
|
38
|
+
return data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 读心跳;文件不存在 / 内容损坏都返回 null。 */
|
|
42
|
+
export function readHostMarker(env = process.env) {
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(fs.readFileSync(hostMarkerPath(env), 'utf8'))
|
|
45
|
+
if (!data || typeof data !== 'object') return null
|
|
46
|
+
return {
|
|
47
|
+
pid: Number(data.pid) || 0,
|
|
48
|
+
at: Number(data.at) || 0,
|
|
49
|
+
version: String(data.version || ''),
|
|
50
|
+
}
|
|
51
|
+
} catch { return null }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 删除心跳(插件 dispose 时尽力而为;宿主被硬杀时靠新鲜度判定兜底)。 */
|
|
55
|
+
export function clearHostMarker(env = process.env) {
|
|
56
|
+
try { fs.unlinkSync(hostMarkerPath(env)); return true } catch { return false }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 本机此刻有没有活着的 DSH 宿主:心跳新鲜(默认 45s,宿主每 15s 刷一次)**且**那个 pid 还在。
|
|
61
|
+
* 两个条件都要,避免「宿主崩了但心跳文件还在」被误判成有宿主。
|
|
62
|
+
*
|
|
63
|
+
* @param {Record<string,string>} [env]
|
|
64
|
+
* @param {{maxAgeMs?:number, now?:number, isAlive?:(pid:number)=>boolean}} [opts]
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
export function hostAlive(env = process.env, { maxAgeMs = 45000, now = Date.now(), isAlive = isProcessAlive } = {}) {
|
|
68
|
+
const marker = readHostMarker(env)
|
|
69
|
+
if (!marker || !marker.pid || !marker.at) return false
|
|
70
|
+
if (now - marker.at > Math.max(0, Number(maxAgeMs) || 0)) return false
|
|
71
|
+
return isAlive(marker.pid)
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"prepublishOnly": "node build.mjs",
|
|
65
65
|
"release": "node scripts/release.mjs",
|
|
66
66
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
67
|
-
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs"
|
|
67
|
+
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|