memory-eternal 0.10.33 → 0.10.35
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 +3 -2
- package/README.md +3 -2
- package/index.js +27 -2
- package/lib/api.js +7 -0
- package/lib/client.js +14 -14
- package/lib/resident-state.js +26 -0
- package/lib/watchdog.js +70 -0
- package/lib/web.js +41 -1
- package/package.json +2 -2
- package/web/app.js +16 -16
package/lib/resident-state.js
CHANGED
|
@@ -45,6 +45,32 @@ export function recordReplaceBlocked(env = process.env, { error = '', lockPath =
|
|
|
45
45
|
try { writeResidentState(env, data); return { ok: true, recordedAt: at, firstTime } } catch { return { ok: false, recordedAt: at, firstTime } }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 记下「常驻实例由**宿主**托管」(issue #30 ④ 的落地):锁不可写时看门狗路径被永久拒绝,
|
|
50
|
+
* 于是宿主自己停掉旧实例、在原端口拉起新的并保活 —— 这条状态让面板/接口知道「虽然锁里没登记,
|
|
51
|
+
* 但实例是有人管的」,而不是显示成「未托管 / 孤儿」。
|
|
52
|
+
*/
|
|
53
|
+
export function recordHostManaged(env = process.env, { port = 0, version = '', served = '', reason = '' } = {}) {
|
|
54
|
+
const at = Date.now()
|
|
55
|
+
const data = { ...readResidentState(env), hostManaged: { at, port: Number(port) || 0, version: String(version || ''), served: String(served || ''), reason: String(reason || '').slice(0, 300), count: (Number((readResidentState(env).hostManaged || {}).count) || 0) + 1 } }
|
|
56
|
+
try { writeResidentState(env, data); return { ok: true, at } } catch { return { ok: false, at } }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 宿主托管状态(默认 24h 内算有效 —— 宿主进程一重启,这条就该被重新确认)。 */
|
|
60
|
+
export function hostManagedInfo(env = process.env, { freshMs = 24 * 3600 * 1000 } = {}) {
|
|
61
|
+
const h = readResidentState(env).hostManaged
|
|
62
|
+
if (!h) return null
|
|
63
|
+
return { ...h, fresh: Date.now() - Number(h.at || 0) < freshMs }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 看门狗重新登记(或用户手工接管)→ 清掉宿主托管状态。 */
|
|
67
|
+
export function clearHostManaged(env = process.env) {
|
|
68
|
+
const cur = readResidentState(env)
|
|
69
|
+
if (!cur.hostManaged) return false
|
|
70
|
+
delete cur.hostManaged
|
|
71
|
+
try { writeResidentState(env, cur); return true } catch { return false }
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
/** 环境修好(锁可写了)→ 清掉这条状态。 */
|
|
49
75
|
export function clearReplaceBlocked(env = process.env) {
|
|
50
76
|
const cur = readResidentState(env)
|
package/lib/watchdog.js
CHANGED
|
@@ -154,6 +154,53 @@ export function releaseWatchdogLock({ env = process.env, pid = process.pid, keep
|
|
|
154
154
|
* 只补本端口、且当前没有**活着的**同端口登记时才写,避免覆盖新实例的登记。
|
|
155
155
|
* @returns {boolean} 是否真的写回
|
|
156
156
|
*/
|
|
157
|
+
/**
|
|
158
|
+
* 「接管助手」的单飞互斥(issue #30 第六轮实测):宿主 tick 与 CLI 会各自拉起一个 `--replace`,
|
|
159
|
+
* 两个助手谁都不写锁、还互相收对方的 web —— 现场更难复盘。用一个 claim 文件做互斥:
|
|
160
|
+
* 助手开工前独占创建;已存在且**持有者还活着 + 没过期**就退出(不抢)。
|
|
161
|
+
*
|
|
162
|
+
* 失败开放(fail-open):受限环境写不了 claim 时**照常进行**(不能让权限问题把接管彻底堵死),
|
|
163
|
+
* 但要留一条可查的理由。
|
|
164
|
+
*/
|
|
165
|
+
export function replaceClaimPath(env = process.env) {
|
|
166
|
+
return path.join(path.dirname(watchdogLockPath(env)), 'memory-eternal-replace.claim')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 读当前 claim(不存在/损坏返回 null)。 */
|
|
170
|
+
export function readReplaceClaim(env = process.env) {
|
|
171
|
+
try {
|
|
172
|
+
const d = JSON.parse(fs.readFileSync(replaceClaimPath(env), 'utf8'))
|
|
173
|
+
if (!d || typeof d !== 'object' || !d.pid) return null
|
|
174
|
+
return d
|
|
175
|
+
} catch { return null }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 抢 claim:抢到 true;有人在跑则是 false(附持有者信息)。 */
|
|
179
|
+
export function claimReplaceSlot({ env = process.env, port = 0, pid = process.pid, staleMs = 60000, now = Date.now() } = {}) {
|
|
180
|
+
const file = replaceClaimPath(env)
|
|
181
|
+
const cur = readReplaceClaim(env)
|
|
182
|
+
if (cur) {
|
|
183
|
+
const fresh = now - Number(cur.at || 0) < Math.max(1000, Number(staleMs) || 60000)
|
|
184
|
+
if (fresh && Number(cur.pid) !== Number(pid) && isPidAlive(cur.pid)) {
|
|
185
|
+
return { claimed: false, holder: cur, reason: '已有接管助手在跑(pid ' + cur.pid + ',' + Math.round((now - Number(cur.at || now)) / 1000) + 's 前)' }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
190
|
+
fs.writeFileSync(file, JSON.stringify({ pid: Number(pid), port: Number(port) || 0, at: now }))
|
|
191
|
+
return { claimed: true, holder: cur || null, reason: cur ? '接管了陈旧 claim' : '抢到 claim' }
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return { claimed: true, holder: cur || null, reason: 'claim 写不进去(' + String((e && e.message) || e) + ')→ 失败开放,继续接管' }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 释放 claim(只清自己那条;别人的不动)。 */
|
|
198
|
+
export function releaseReplaceClaim({ env = process.env, pid = process.pid } = {}) {
|
|
199
|
+
const cur = readReplaceClaim(env)
|
|
200
|
+
if (cur && Number(cur.pid) !== Number(pid)) return false
|
|
201
|
+
try { fs.unlinkSync(replaceClaimPath(env)); return true } catch { return false }
|
|
202
|
+
}
|
|
203
|
+
|
|
157
204
|
export function restoreWatchdogLock({ env = process.env, port, watchdogs = [] } = {}) {
|
|
158
205
|
const p = Number(port) || 0
|
|
159
206
|
const want = (Array.isArray(watchdogs) ? watchdogs : []).filter((w) => Number(w.port) === p)
|
|
@@ -782,6 +829,19 @@ export async function restartResident({
|
|
|
782
829
|
}
|
|
783
830
|
}
|
|
784
831
|
}
|
|
832
|
+
// 单飞互斥(issue #30 第六轮实测):接管还在进行中时**不要再拉第二个助手** ——
|
|
833
|
+
// 宿主 tick 与 CLI 同时来的时候,两个助手会互相收对方的 web,现场反而更难复盘。
|
|
834
|
+
if (!deps.ignoreClaim) {
|
|
835
|
+
try {
|
|
836
|
+
const inflight = readReplaceClaim(env)
|
|
837
|
+
if (inflight && Number(inflight.pid) !== process.pid && isPidAlive(inflight.pid) && Date.now() - Number(inflight.at || 0) < 60000) {
|
|
838
|
+
return {
|
|
839
|
+
ok: false, stage: 'in-flight', expect, served: '', waitedMs: 0, helperPid: 0, before, lockRestored: false, occupant: null,
|
|
840
|
+
error: '已有接管正在进行中(pid ' + inflight.pid + ',' + Math.round((Date.now() - Number(inflight.at || Date.now())) / 1000) + 's 前启动)→ 本次不再重复拉起助手;稍后可再试 dsh-memory restart --port ' + p,
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
} catch { /* 读不到 claim 就当没有 */ }
|
|
844
|
+
}
|
|
785
845
|
let helperPid = 0
|
|
786
846
|
try {
|
|
787
847
|
const child = (deps.spawnHelper || spawnReplaceHelper)({ port: p, vaultRoot, interval, maxRestart })
|
|
@@ -1302,6 +1362,15 @@ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
|
|
|
1302
1362
|
try { fs.writeSync(2, `[watchdog] ${msg}\n`) } catch { log(msg) }
|
|
1303
1363
|
process.exitCode = 0
|
|
1304
1364
|
} else if (argv.includes('--replace')) {
|
|
1365
|
+
// 单飞互斥(issue #30 第六轮实测:宿主 tick 与 CLI 各拉一个助手互相踩):
|
|
1366
|
+
// 抢不到 claim 就**直接退出**,把接管让给已经在跑的那个;抢到就在退出时释放。
|
|
1367
|
+
const slotClaim = claimReplaceSlot({ port })
|
|
1368
|
+
if (!slotClaim.claimed) {
|
|
1369
|
+
log('跳过本次接管:' + slotClaim.reason + '(避免两个助手互相收对方的 web)')
|
|
1370
|
+
process.exitCode = 0
|
|
1371
|
+
} else {
|
|
1372
|
+
if (slotClaim.holder) log(slotClaim.reason)
|
|
1373
|
+
process.on('exit', () => { try { releaseReplaceClaim() } catch { /* 无妨 */ } })
|
|
1305
1374
|
// 版本漂移自愈:先收掉旧常驻实例,再作为新一代看门狗接管(见 clearResidentForTakeover 的说明)。
|
|
1306
1375
|
const cleared = await clearResidentForTakeover({ port })
|
|
1307
1376
|
for (const w of cleared.warnings) log('⚠ ' + w)
|
|
@@ -1323,6 +1392,7 @@ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
|
|
|
1323
1392
|
}
|
|
1324
1393
|
await noteReplace('boot', summary)
|
|
1325
1394
|
bootWatchdog()
|
|
1395
|
+
}
|
|
1326
1396
|
} else {
|
|
1327
1397
|
bootWatchdog()
|
|
1328
1398
|
}
|
package/lib/web.js
CHANGED
|
@@ -281,10 +281,50 @@ async function waitForOurWeb(port, timeoutMs = 6000, intervalMs = 500) {
|
|
|
281
281
|
* 端口被非本项目进程占用时向后漂移(+1..+10)。
|
|
282
282
|
* @returns {Promise<{url:string, port:number, spawned:boolean}>}
|
|
283
283
|
*/
|
|
284
|
-
|
|
284
|
+
/**
|
|
285
|
+
* 强制接管端口上的**自家旧 web**:收掉它、等端口空出来(issue #30 第四点:锁不可写的机器上
|
|
286
|
+
* 看门狗路径被永久拒绝,只能由**宿主**自己把常驻实例换掉 —— 宿主能写、也能 kill 自己人的进程)。
|
|
287
|
+
*
|
|
288
|
+
* 安全闸门:只对 `looksLikeOurWeb(command)` 为真的占用者下手;别人的进程一个都不许动。
|
|
289
|
+
*
|
|
290
|
+
* @returns {Promise<{took:boolean, pid:number, reason:string}>}
|
|
291
|
+
*/
|
|
292
|
+
export async function takeoverOwnWeb(port, { findListener, kill, sleep, looksLike, timeoutMs = 4000 } = {}) {
|
|
293
|
+
const p = Number(port) || DEFAULT_WEB_PORT
|
|
294
|
+
const find = findListener || (await import('./watchdog.js')).findPortListener
|
|
295
|
+
const isOurs = looksLike || looksLikeOurWeb
|
|
296
|
+
const wait = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
297
|
+
const killFn = kill || ((pid) => { try { process.kill(pid, 'SIGTERM'); return true } catch { return false } })
|
|
298
|
+
let occ = null
|
|
299
|
+
try { occ = await find(p) } catch { occ = null }
|
|
300
|
+
if (!occ || !occ.pid) return { took: false, pid: 0, reason: '端口上没有占用者' }
|
|
301
|
+
if (!isOurs(occ.command)) return { took: false, pid: Number(occ.pid), reason: '占用者不是本插件的 web(' + (occ.command || '命令行未知') + ')→ 不动它' }
|
|
302
|
+
let alive = true
|
|
303
|
+
try { alive = await isOurs(p) } catch { alive = true }
|
|
304
|
+
void alive
|
|
305
|
+
if (!killFn(occ.pid)) return { took: false, pid: Number(occ.pid), reason: '终止失败(可能无权限)' }
|
|
306
|
+
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0)
|
|
307
|
+
while (Date.now() < deadline) {
|
|
308
|
+
await wait(200)
|
|
309
|
+
let left = null
|
|
310
|
+
try { left = await find(p) } catch { left = null }
|
|
311
|
+
if (!left || !left.pid) return { took: true, pid: Number(occ.pid), reason: '已收掉自家旧 web' }
|
|
312
|
+
}
|
|
313
|
+
return { took: false, pid: Number(occ.pid), reason: '已发出终止但端口仍被占用' }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), spawnEnv = {}, totalTimeoutMs = 15000, takeoverWaitMs = 6000, forceTakeover = false, deps = {} } = {}) {
|
|
285
317
|
// 探测可用端口:先找「已是我们的服务」的端口,再找空闲端口
|
|
286
318
|
let target = port
|
|
287
319
|
let found = null
|
|
320
|
+
// 强制接管(issue #30 ④):宿主自管的机器上,先把自家旧实例收掉,再在原端口起新的 ——
|
|
321
|
+
// 不这么做就会走到下面的「让位到 port+1」,于是既换不掉旧的、又多出一个实例。
|
|
322
|
+
if (forceTakeover) {
|
|
323
|
+
try {
|
|
324
|
+
const r = await takeoverOwnWeb(port, { findListener: deps.findListener, kill: deps.kill, sleep: deps.sleep, timeoutMs: Math.max(1500, Math.min(5000, totalTimeoutMs)) })
|
|
325
|
+
process.stderr.write(`[memory-eternal] 强制接管端口 ${port}:${r.reason}${r.pid ? '(pid ' + r.pid + ')' : ''}\n`)
|
|
326
|
+
} catch (e) { process.stderr.write(`[memory-eternal] 强制接管失败:${String((e && e.message) || e)}\n`) }
|
|
327
|
+
}
|
|
288
328
|
for (let p = port; p < port + 10; p++) {
|
|
289
329
|
const alive = await probeWebServer(p)
|
|
290
330
|
if (alive) { found = { url: `http://127.0.0.1:${p}`, port: p, spawned: false }; break }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.35",
|
|
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 && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.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 && 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 && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|