memory-eternal 0.10.8 → 0.10.10
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 +12 -7
- package/README.md +11 -7
- package/index.js +40 -4
- package/lib/client.js +17 -17
- package/lib/config-sync.js +5 -1
- package/lib/watchdog.js +9 -2
- package/lib/web.js +40 -3
- package/package.json +2 -2
- package/web/app.js +24 -24
package/lib/config-sync.js
CHANGED
|
@@ -45,7 +45,7 @@ export function readPendingConfig(env = process.env) {
|
|
|
45
45
|
* @param {string} text 完整内容
|
|
46
46
|
* @returns {string} 目标文件路径
|
|
47
47
|
*/
|
|
48
|
-
export function writeFileAtomicSync(file, text, { retries =
|
|
48
|
+
export function writeFileAtomicSync(file, text, { retries = 12, delayMs = 25, onDegraded } = {}) {
|
|
49
49
|
const tmp = file + '.' + process.pid + '.tmp'
|
|
50
50
|
fs.writeFileSync(tmp, text, 'utf8')
|
|
51
51
|
const retriable = (code) => code === 'EPERM' || code === 'EBUSY' || code === 'EACCES'
|
|
@@ -60,6 +60,10 @@ export function writeFileAtomicSync(file, text, { retries = 8, delayMs = 20 } =
|
|
|
60
60
|
if (i < retries) { sleepSync(delayMs); continue }
|
|
61
61
|
// 退避仍失败(极端争用):退回直接覆盖写。宁可短暂退化到非原子,也不能把用户的
|
|
62
62
|
// 配置改动整份丢掉 —— 这是这个函数存在的第一原则。
|
|
63
|
+
//
|
|
64
|
+
// 这是**可观测的退化**:并发读方在这个窗口里可能读到半截。tests/config-sync 的并发用例
|
|
65
|
+
// 靠 onDegraded 区分「原子路径失效」与「OS 拒绝了替换」—— 前者仍然必须红。
|
|
66
|
+
if (typeof onDegraded === 'function') { try { onDegraded(file) } catch { /* 回调异常不影响写入 */ } }
|
|
63
67
|
fs.writeFileSync(file, text, 'utf8')
|
|
64
68
|
try { fs.unlinkSync(tmp) } catch { /* 已清理 */ }
|
|
65
69
|
return file
|
package/lib/watchdog.js
CHANGED
|
@@ -448,12 +448,19 @@ export async function waitForServedVersion(port, expect, { timeoutMs = 15000, in
|
|
|
448
448
|
* @returns {Promise<{port:number, served:string, occupant:{pid:number,command:string}|null,
|
|
449
449
|
* occupantIsOurs:boolean, watchdog:object|null, watchdogAlive:boolean, residentStartedAt:number}>}
|
|
450
450
|
*/
|
|
451
|
-
export async function inspectResident({ env = process.env, port, probeVersion, findListener } = {}) {
|
|
451
|
+
export async function inspectResident({ env = process.env, port, probeVersion, findListener, retries = 3, retryDelayMs = 500, sleepFn } = {}) {
|
|
452
452
|
const p = Number(port) || 0
|
|
453
453
|
const entry = readWatchdogLock(env).watchdogs.find((w) => Number(w.port) === p && isPidAlive(w.pid)) || null
|
|
454
454
|
let served = ''
|
|
455
455
|
if (p) {
|
|
456
|
-
|
|
456
|
+
// issue #29 建议 3:一次探针打空(实例正忙 / GC / 冷启动)**不能**当成「旧版不自报版本」——
|
|
457
|
+
// 那个结论会直接触发漂移替换,于是每次启动都多起一次常驻实例。退避重试后再下结论。
|
|
458
|
+
const wait = sleepFn || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
459
|
+
for (let i = 0; ; i++) {
|
|
460
|
+
try { served = String((await (probeVersion || probeServedVersion)(p)) || '') } catch { served = '' }
|
|
461
|
+
if (served || i >= Math.max(0, Number(retries) || 0)) break
|
|
462
|
+
await wait(Math.max(0, Number(retryDelayMs) || 0))
|
|
463
|
+
}
|
|
457
464
|
}
|
|
458
465
|
let occupant = null
|
|
459
466
|
// 只有「探不到版本」时才需要问端口占用者:能自报版本就已经证明是我们的服务了
|
package/lib/web.js
CHANGED
|
@@ -246,13 +246,42 @@ export async function probeWebServer(port = DEFAULT_WEB_PORT, timeoutMs = 800) {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* 宽松判定:这个端口上是不是**自家的 web**(旧版可能不自报 version / vaultDir,严格探针会判否)。
|
|
251
|
+
*
|
|
252
|
+
* 为什么需要(issue #29):升级期端口被**自家旧包**的 detached 实例占着时,严格探针认不出它,
|
|
253
|
+
* `ensureWebServer()` 就会向后漂到一个空端口 → 一段窗口里同时存在两个实例、两个端口,
|
|
254
|
+
* 而宿主记录的 `web-info` 指向的可能是随后被自愈收掉的那个。多探几个自家路由即可识别。
|
|
255
|
+
*/
|
|
256
|
+
export async function looksLikeOurWeb(port = DEFAULT_WEB_PORT, timeoutMs = 600) {
|
|
257
|
+
for (const suffix of ['/overview', '/web-info', '/budget']) {
|
|
258
|
+
try {
|
|
259
|
+
const res = await fetch(`http://127.0.0.1:${port}${API_PREFIX}${suffix}`, { signal: AbortSignal.timeout(timeoutMs) })
|
|
260
|
+
if (!res.ok) continue
|
|
261
|
+
const data = await res.json().catch(() => null)
|
|
262
|
+
if (data && typeof data === 'object' && data.ok === true) return true
|
|
263
|
+
} catch { /* 换下一个探针 */ }
|
|
264
|
+
}
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 等端口上的自家实例变成「可严格识别」(自愈替换完成)—— 期间它可能被清掉再拉起。 */
|
|
269
|
+
async function waitForOurWeb(port, timeoutMs = 6000, intervalMs = 500) {
|
|
270
|
+
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0)
|
|
271
|
+
for (;;) {
|
|
272
|
+
if (await probeWebServer(port)) return true
|
|
273
|
+
if (Date.now() >= deadline) return false
|
|
274
|
+
await new Promise((r) => setTimeout(r, Math.max(100, Number(intervalMs) || 500)))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
249
278
|
/**
|
|
250
279
|
* 确保 web server 存活(默认开启的实现核心)。
|
|
251
280
|
* 已活 → 直接返回 URL;未活 → detached spawn 独立进程并轮询探活。
|
|
252
281
|
* 端口被非本项目进程占用时向后漂移(+1..+10)。
|
|
253
282
|
* @returns {Promise<{url:string, port:number, spawned:boolean}>}
|
|
254
283
|
*/
|
|
255
|
-
export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), spawnEnv = {}, totalTimeoutMs = 15000 } = {}) {
|
|
284
|
+
export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), spawnEnv = {}, totalTimeoutMs = 15000, takeoverWaitMs = 6000 } = {}) {
|
|
256
285
|
// 探测可用端口:先找「已是我们的服务」的端口,再找空闲端口
|
|
257
286
|
let target = port
|
|
258
287
|
let found = null
|
|
@@ -260,8 +289,16 @@ export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = def
|
|
|
260
289
|
const alive = await probeWebServer(p)
|
|
261
290
|
if (alive) { found = { url: `http://127.0.0.1:${p}`, port: p, spawned: false }; break }
|
|
262
291
|
// 该端口不是我们的服务:若连不上(空闲)就用它;连得上但不是我们的(被占用)则漂移
|
|
263
|
-
|
|
264
|
-
|
|
292
|
+
if (await isPortConnectable(p) === false) { target = p; break }
|
|
293
|
+
// issue #29:占用者「像自家的 web、但严格探针认不出」(升级期旧包的 detached 实例)时,
|
|
294
|
+
// **不要立刻漂到 port+1** —— 自愈(restartResident)马上会把配置端口上的实例换成新版,
|
|
295
|
+
// 这时另起一个端口只会多出一个实例、并让 web-info 指向随后被收掉的那个(客户端 iframe
|
|
296
|
+
// 于是「拒绝连接」)。先等它接管;等不到再漂,并留下一条能查的日志。
|
|
297
|
+
if (await looksLikeOurWeb(p)) {
|
|
298
|
+
if (await waitForOurWeb(p, takeoverWaitMs)) { found = { url: `http://127.0.0.1:${p}`, port: p, spawned: false }; break }
|
|
299
|
+
if (await isPortConnectable(p) === false) { target = p; break }
|
|
300
|
+
process.stderr.write(`[memory-eternal] 端口 ${p} 被自家旧实例占着且 ${takeoverWaitMs}ms 内没让位 → 向后找空闲端口(web-info 会实时收敛,issue #29)\n`)
|
|
301
|
+
}
|
|
265
302
|
target = p + 1
|
|
266
303
|
}
|
|
267
304
|
if (found) return found
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
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 && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.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 && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|