memory-eternal 0.9.23 → 0.10.2
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 +2 -16
- package/.codex-plugin/plugin.json +2 -16
- package/.cursor-plugin/plugin.json +2 -16
- package/.mcp.json +9 -9
- package/LICENSE +21 -21
- package/README.en.md +27 -7
- package/README.md +40 -7
- package/bin/dsh-memory.mjs +318 -188
- package/examples/mcp-memory/memory-eternal.cordis.yml +59 -0
- package/hooks/capture.mjs +77 -77
- package/hooks/claude-session.js +56 -56
- package/hooks/hooks.claude.json +12 -12
- package/hooks/hooks.codex.json +15 -15
- package/hooks/hooks.cursor.json +11 -11
- package/index.js +187 -32
- package/lib/api.js +592 -536
- package/lib/capture-log.js +65 -59
- package/lib/capture-run.js +134 -126
- package/lib/capture.js +278 -14
- package/lib/client.js +20 -17
- package/lib/config-sync.js +161 -89
- package/lib/db.js +359 -212
- package/lib/feedback.js +214 -205
- package/lib/llm-openai.js +84 -84
- package/lib/mcp.js +197 -166
- package/lib/node-bin.js +99 -99
- package/lib/setup.js +470 -470
- package/lib/sse.js +51 -51
- package/lib/stall.js +30 -30
- package/lib/sweep.js +56 -56
- package/lib/vault-resolve.js +84 -84
- package/lib/vault.js +347 -106
- package/lib/watchdog.js +527 -392
- package/lib/web-assets.js +41 -0
- package/lib/web.js +316 -176
- package/package.json +12 -7
- package/scripts/release.mjs +204 -0
- package/skills/memory-recall/SKILL.md +14 -14
- package/web/app.js +20 -17
- package/web/index.html +46 -46
package/lib/config-sync.js
CHANGED
|
@@ -1,89 +1,161 @@
|
|
|
1
|
-
// 配置同步(共享文件协议):让「独立 Web 页」也能保存配置。
|
|
2
|
-
//
|
|
3
|
-
// 背景:独立 web server 进程里没有 DSH 的 settings 服务,`ctx.settings.update` 不存在,
|
|
4
|
-
// 所以它的 /config 过去只能只读 —— 用户点保存永远没反应(issue #12 的另一半)。
|
|
5
|
-
// 方案:独立端把改动写进 `memory-eternal-config.pending.json`,DSH 宿主激活时与运行期
|
|
6
|
-
// 轮询该文件,用 settings.update 应用后删除。DSH 没运行时改动留在文件里,下次启动生效。
|
|
7
|
-
import fs from 'node:fs'
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import { configFilePath } from './capture-run.js'
|
|
10
|
-
|
|
11
|
-
/** 待应用配置文件的路径(与共享配置文件同目录)。 */
|
|
12
|
-
export function pendingConfigPath(env = process.env) {
|
|
13
|
-
return String(configFilePath(env)).replace(/\.json$/i, '.pending.json')
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
1
|
+
// 配置同步(共享文件协议):让「独立 Web 页」也能保存配置。
|
|
2
|
+
//
|
|
3
|
+
// 背景:独立 web server 进程里没有 DSH 的 settings 服务,`ctx.settings.update` 不存在,
|
|
4
|
+
// 所以它的 /config 过去只能只读 —— 用户点保存永远没反应(issue #12 的另一半)。
|
|
5
|
+
// 方案:独立端把改动写进 `memory-eternal-config.pending.json`,DSH 宿主激活时与运行期
|
|
6
|
+
// 轮询该文件,用 settings.update 应用后删除。DSH 没运行时改动留在文件里,下次启动生效。
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { configFilePath } from './capture-run.js'
|
|
10
|
+
|
|
11
|
+
/** 待应用配置文件的路径(与共享配置文件同目录)。 */
|
|
12
|
+
export function pendingConfigPath(env = process.env) {
|
|
13
|
+
return String(configFilePath(env)).replace(/\.json$/i, '.pending.json')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 读待应用改动;文件不存在/损坏都返回 null(不影响主流程)。
|
|
18
|
+
*
|
|
19
|
+
* 除 patch 之外还带出应用状态:tries(连续失败次数)、dropped(已放弃)、lastError
|
|
20
|
+
* (最近一次失败原因)—— #16 之前这些信息只存在文件里却没人看,失败只能靠人肉盯 tries。
|
|
21
|
+
*/
|
|
22
|
+
export function readPendingConfig(env = process.env) {
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(fs.readFileSync(pendingConfigPath(env), 'utf8'))
|
|
25
|
+
if (!data || typeof data.patch !== 'object' || data.patch === null || Array.isArray(data.patch)) return null
|
|
26
|
+
return {
|
|
27
|
+
at: Number(data.at) || 0,
|
|
28
|
+
tries: Number(data.tries) || 0,
|
|
29
|
+
dropped: data.dropped === true,
|
|
30
|
+
lastError: typeof data.lastError === 'string' ? data.lastError : '',
|
|
31
|
+
patch: data.patch,
|
|
32
|
+
}
|
|
33
|
+
} catch { return null }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 原子写文本文件(同目录 tmp + rename)。
|
|
38
|
+
*
|
|
39
|
+
* 为什么必须原子:这些文件是**跨进程共享的读源**(独立 web 的 lib/web.js 每次请求都
|
|
40
|
+
* JSON.parse 共享配置、MCP hook 也读)。非原子的 writeFile/writeFileSync 是「先截断再写入」,
|
|
41
|
+
* 读方在窗口内会读到空文件或半截内容 → JSON.parse 失败 → 调用方静默回落成默认值。
|
|
42
|
+
* 同目录 rename 在同一文件系统上是原子的:读方要么看到旧的完整文件,要么看到新的完整文件。
|
|
43
|
+
*
|
|
44
|
+
* @param {string} file 目标文件绝对路径
|
|
45
|
+
* @param {string} text 完整内容
|
|
46
|
+
* @returns {string} 目标文件路径
|
|
47
|
+
*/
|
|
48
|
+
export function writeFileAtomicSync(file, text, { retries = 8, delayMs = 20 } = {}) {
|
|
49
|
+
const tmp = file + '.' + process.pid + '.tmp'
|
|
50
|
+
fs.writeFileSync(tmp, text, 'utf8')
|
|
51
|
+
const retriable = (code) => code === 'EPERM' || code === 'EBUSY' || code === 'EACCES'
|
|
52
|
+
for (let i = 0; ; i++) {
|
|
53
|
+
try {
|
|
54
|
+
fs.renameSync(tmp, file)
|
|
55
|
+
return file
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!retriable(error && error.code)) { try { fs.unlinkSync(tmp) } catch { /* 已清理 */ } throw error }
|
|
58
|
+
// Windows 实测:目标文件正被另一个进程读取时,rename 会 EPERM/EBUSY(Windows 的
|
|
59
|
+
// 「不能替换被打开的文件」语义)。短暂退避后重试 —— 读方打开文件都是短窗口。
|
|
60
|
+
if (i < retries) { sleepSync(delayMs); continue }
|
|
61
|
+
// 退避仍失败(极端争用):退回直接覆盖写。宁可短暂退化到非原子,也不能把用户的
|
|
62
|
+
// 配置改动整份丢掉 —— 这是这个函数存在的第一原则。
|
|
63
|
+
fs.writeFileSync(file, text, 'utf8')
|
|
64
|
+
try { fs.unlinkSync(tmp) } catch { /* 已清理 */ }
|
|
65
|
+
return file
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 同步 sleep(不烧 CPU):同步 API 里做退避重试要用它,而不是忙等。 */
|
|
71
|
+
function sleepSync(ms) {
|
|
72
|
+
const timeout = Math.max(0, Number(ms) || 0)
|
|
73
|
+
if (!timeout) return
|
|
74
|
+
try {
|
|
75
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, timeout)
|
|
76
|
+
} catch {
|
|
77
|
+
const end = Date.now() + timeout
|
|
78
|
+
while (Date.now() < end) { /* 兜底忙等(老运行时没有 Atomics.wait) */ }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 原子写入待应用改动,返回写入内容。
|
|
84
|
+
* @param {number} [tries] 连续失败次数
|
|
85
|
+
* @param {{dropped?:boolean,lastError?:string}} [state] 失败状态(#16:不再静默删文件,改为标注)
|
|
86
|
+
*/
|
|
87
|
+
export function writePendingConfig(env, patch, now = Date.now(), tries = 0, state = {}) {
|
|
88
|
+
const file = pendingConfigPath(env)
|
|
89
|
+
const data = { at: now, ...(tries > 0 ? { tries } : {}), patch }
|
|
90
|
+
if (state.lastError) data.lastError = String(state.lastError)
|
|
91
|
+
if (state.dropped) data.dropped = true
|
|
92
|
+
writeFileAtomicSync(file, JSON.stringify(data, null, 2))
|
|
93
|
+
return data
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 删除待应用文件;不存在也算成功。 */
|
|
97
|
+
export function clearPendingConfig(env = process.env) {
|
|
98
|
+
try { fs.unlinkSync(pendingConfigPath(env)); return true } catch { return false }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 监听待应用文件的变化(毫秒级触发),用于把「独立页保存 → DSH 应用」从 5 秒轮询
|
|
103
|
+
* 提升到准即时。监听失败(平台/权限)时退化为不监听 —— 仍有 5 秒轮询兜底。
|
|
104
|
+
* @returns {() => void} 停止监听
|
|
105
|
+
*/
|
|
106
|
+
export function watchPendingConfig(env, onChange, { debounceMs = 60 } = {}) {
|
|
107
|
+
const file = pendingConfigPath(env)
|
|
108
|
+
let timer = null
|
|
109
|
+
let watcher = null
|
|
110
|
+
const fire = () => {
|
|
111
|
+
if (timer) clearTimeout(timer)
|
|
112
|
+
timer = setTimeout(() => { try { onChange() } catch { /* 回调异常不影响监听 */ } }, debounceMs)
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
// 监听**文件本身**而不是父目录:
|
|
116
|
+
// 父目录被删除 / 重命名时(临时 HOME、清理脚本、测试收尾),Windows 上 libuv 会断言崩溃
|
|
117
|
+
// (`Assertion failed: !_wcsnicmp(filename, dir, dirlen), src\win\fs-event.c`),
|
|
118
|
+
// 而监听文件时目录消失只会得到 ENOENT,不会带崩进程。也顺带免掉了按文件名过滤。
|
|
119
|
+
watcher = fs.watch(file, () => fire())
|
|
120
|
+
if (typeof watcher.unref === 'function') watcher.unref()
|
|
121
|
+
} catch { return () => {} }
|
|
122
|
+
return () => {
|
|
123
|
+
if (timer) clearTimeout(timer)
|
|
124
|
+
try { watcher.close() } catch { /* 已关闭 */ }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 取出并应用一次待应用改动:apply(patch) 成功后删除文件;失败保留(下轮重试)。
|
|
130
|
+
*
|
|
131
|
+
* #16 的两处修正:
|
|
132
|
+
* 1. 重试耗尽后**不再静默删除**用户的改动 —— 改为在文件里标 dropped:true + lastError,
|
|
133
|
+
* 并抛出一个说明「改动还在哪个文件里」的错误(由调用方写日志)。丢用户改动是这里
|
|
134
|
+
* 最不该有的行为,保留文件也让「反馈异常」能带上现场。
|
|
135
|
+
* 2. 已标记 dropped 的文件不再重试(否则每 5 秒无限重试),但文件继续留着作为证据;
|
|
136
|
+
* 用户在独立 Web 端重新保存会写入一份新的 pending 并清除该标记。
|
|
137
|
+
*
|
|
138
|
+
* @returns {Promise<object|null>} 应用成功的 patch;没有待应用项 / 已放弃时返回 null
|
|
139
|
+
*/
|
|
140
|
+
export async function drainPendingConfig(env, apply, maxTries = 5) {
|
|
141
|
+
const pending = readPendingConfig(env)
|
|
142
|
+
if (!pending) return null
|
|
143
|
+
if (pending.dropped) return null
|
|
144
|
+
try {
|
|
145
|
+
await apply(pending.patch)
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const tries = (pending.tries || 0) + 1
|
|
148
|
+
const reason = String((error && error.message) || error)
|
|
149
|
+
if (tries >= maxTries) {
|
|
150
|
+
writePendingConfig(env, pending.patch, pending.at, tries, { dropped: true, lastError: reason })
|
|
151
|
+
const dropped = new Error('待应用配置连续失败 ' + tries + ' 次已放弃(原报错:' + reason + ');改动仍保留在 ' + pendingConfigPath(env) + ',可修正后重新保存')
|
|
152
|
+
dropped.dropped = true
|
|
153
|
+
dropped.lastError = reason
|
|
154
|
+
throw dropped
|
|
155
|
+
}
|
|
156
|
+
writePendingConfig(env, pending.patch, pending.at, tries, { lastError: reason })
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
clearPendingConfig(env)
|
|
160
|
+
return pending.patch
|
|
161
|
+
}
|