memory-eternal 0.10.32 → 0.10.34

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.
@@ -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
@@ -533,14 +533,23 @@ export async function probeServedVersion(port, timeoutMs = 1200) {
533
533
  // issue #31/#33 第②层:`/overview` 会碰数据库,库只读/损坏时它 500 —— 于是「端口上明明活着、
534
534
  // 版本却拿不到」:verify 报「(无响应)」、status 甚至误判成「v0.10.4 之前的旧版」,进而触发
535
535
  // 一轮又一轮无效替换。`/version-check` **不碰库**(只读 package.json + 缓存 + 自探),先问它。
536
- try {
537
- const v = await probeViaVersionCheck(port, timeoutMs)
538
- if (v) return v
539
- } catch { /* 回落到 /overview */ }
540
- try {
541
- const info = await probe(port, timeoutMs)
542
- return info && typeof info.version === 'string' ? info.version : ''
543
- } catch { return '' }
536
+ //
537
+ // 再叠一层**重试**(issue #30 第五轮实测):报告者看到「同一时刻 version-check 200、2 秒后
538
+ // status 说没有服务」—— 单发探测撞上重启窗口/瞬时拒绝就会得出相反结论。这里退避重试 3 次
539
+ // (每次约 150/300ms),让**只调一次**的那些调用方(status / inspectResident / 面板)不再被瞬时抖动骗到。
540
+ const tries = 3
541
+ for (let i = 0; i < tries; i++) {
542
+ try {
543
+ const v = await probeViaVersionCheck(port, timeoutMs)
544
+ if (v) return v
545
+ } catch { /* 回落到 /overview */ }
546
+ try {
547
+ const info = await probe(port, timeoutMs)
548
+ if (info && typeof info.version === 'string' && info.version) return info.version
549
+ } catch { /* 继续重试 */ }
550
+ if (i < tries - 1) await new Promise((r) => setTimeout(r, 150 * (i + 1)))
551
+ }
552
+ return ''
544
553
  }
545
554
 
546
555
  /** 问 `/version-check` 要「端口上那个进程加载的版本」(不碰数据库;旧版没有该路由时返回空串)。 */
@@ -604,7 +613,8 @@ export async function waitForTakeover(port, expect, {
604
613
  const start = Date.now()
605
614
  let version = ''
606
615
  for (;;) {
607
- try { version = String((await probeVersion(p)) || '') } catch { version = '' }
616
+ // 单次给较小的超时:内层 probeServedVersion 自己会退避重试,外层还要轮询多轮(别把窗口吃光)
617
+ try { version = String((await probeVersion(p, 800)) || '') } catch { version = '' }
608
618
  let fresh = []
609
619
  try {
610
620
  fresh = lockOf(env).watchdogs.filter((w) => Number(w.port) === p && isPidAlive(w.pid) && !beforePids.has(Number(w.pid)))
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
- export async function ensureWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVaultDir(), spawnEnv = {}, totalTimeoutMs = 15000, takeoverWaitMs = 6000 } = {}) {
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.32",
3
+ "version": "0.10.34",
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"
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"
68
68
  },
69
69
  "dependencies": {
70
70
  "@deepseek-ai/schemastery": "^3.18.1"