memory-eternal 0.10.7 → 0.10.8

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.
@@ -319,3 +319,78 @@ export async function drainPendingConfig(env, apply, maxTries = 5) {
319
319
  clearPendingConfig(env)
320
320
  return pending.patch
321
321
  }
322
+
323
+ // ---- 方案 A:把「应用 pending」搬进宿主允许的上下文(2026-10-08 实测结论)----------------
324
+ //
325
+ // 实测(同一宿主机、同一个 settings.update):
326
+ // · 从插件**回调**(setInterval / fs.watch)里调 → 抛「HMR transactions cannot be nested」,
327
+ // 且**一个字节都没提交**(与 patch 内容无关:只含宿主认识的键、值真的变了,同样失败);
328
+ // · 从插件**HTTP 请求处理器**里调 → 真的提交(130→132→130,revision 4→6)。
329
+ // 所以修法不是「换个 API」,而是**换调用上下文**:独立端保存后,请宿主在这条路由里应用 pending。
330
+
331
+ /** 宿主 drain 路由的令牌请求头(令牌写在心跳文件里,只有本机进程读得到)。 */
332
+ export const DRAIN_TOKEN_HEADER = 'x-memory-eternal-token'
333
+
334
+ /** 这个地址算不算「本机」:IPv4/IPv6 回环,含 IPv4-mapped 形式。 */
335
+ export function isLoopbackAddress(address) {
336
+ const s = String(address || '').trim().toLowerCase()
337
+ if (!s) return false
338
+ if (s === 'localhost' || s === '::1' || s === '0:0:0:0:0:0:0:1') return true
339
+ const v4 = s.startsWith('::ffff:') ? s.slice(7) : s
340
+ return /^127\./.test(v4)
341
+ }
342
+
343
+ /**
344
+ * 宿主侧:校验一次「请立刻应用 pending」的调用。放行返回空串,否则返回人话原因。
345
+ *
346
+ * 为什么两件都要验:宿主上的插件 API 是**无鉴权**的(任何本机页面都能 POST),所以这里必须同时
347
+ * 卡「只接受本机来源」与「心跳里的令牌」——令牌只写在共享配置目录的心跳文件里,远端页面拿不到。
348
+ */
349
+ export function drainRequestError({ method, headers, remoteAddress } = {}, token = '') {
350
+ if (String(method || '').toUpperCase() !== 'POST') return '需 POST'
351
+ if (!isLoopbackAddress(remoteAddress)) return '只接受本机请求'
352
+ if (!token) return '宿主未准备令牌'
353
+ const h = headers || {}
354
+ const got = String(h[DRAIN_TOKEN_HEADER] || h['X-Memory-Eternal-Token'] || '')
355
+ if (got !== token) return '令牌不匹配'
356
+ return ''
357
+ }
358
+
359
+ /**
360
+ * 独立端:请宿主**在 HTTP 请求上下文里**立刻应用 pending(方案 A 的客户端一半)。
361
+ *
362
+ * 失败分两类,调用方要区别对待:
363
+ * · status 404/405/0(旧宿主没这条路由 / 连不上)→ 退回「等宿主自己 drain」的老路,不是错误;
364
+ * · 其它 → 真错误(令牌不匹配、应用失败),把 body.error 如实报给用户。
365
+ *
366
+ * @param {{port?:number, token?:string, host?:string, timeoutMs?:number, fetchImpl?:Function}} opts
367
+ * @returns {Promise<{ok:boolean, status:number, body?:object, error?:string}>}
368
+ */
369
+ export async function triggerHostDrain({ port, token, host = '127.0.0.1', timeoutMs = 2000, fetchImpl } = {}) {
370
+ const listenPort = Number(port) || 0
371
+ if (!listenPort || !token) return { ok: false, status: 0, error: '宿主端口/令牌未知(旧宿主,或心跳还没刷新)' }
372
+ const doFetch = fetchImpl || (typeof fetch === 'function' ? fetch : null)
373
+ if (!doFetch) return { ok: false, status: 0, error: '当前运行时没有 fetch' }
374
+ const url = 'http://' + host + ':' + listenPort + '/memory-eternal/api/drain-pending'
375
+ const controller = typeof AbortController === 'function' ? new AbortController() : null
376
+ const timer = controller ? setTimeout(() => { try { controller.abort() } catch { /* 已中止 */ } }, Math.max(0, Number(timeoutMs) || 0)) : null
377
+ try {
378
+ const res = await doFetch(url, {
379
+ method: 'POST',
380
+ headers: { [DRAIN_TOKEN_HEADER]: String(token) },
381
+ ...(controller ? { signal: controller.signal } : {}),
382
+ })
383
+ const text = await res.text()
384
+ let body = null
385
+ try { body = text ? JSON.parse(text) : null } catch { body = null }
386
+ const status = Number(res.status) || 0
387
+ if (!res.ok || !body || body.ok !== true) {
388
+ return { ok: false, status, body, error: (body && body.error) || ('HTTP ' + status) }
389
+ }
390
+ return { ok: true, status, body }
391
+ } catch (error) {
392
+ return { ok: false, status: 0, error: String((error && error.message) || error) }
393
+ } finally {
394
+ if (timer) clearTimeout(timer)
395
+ }
396
+ }
@@ -47,6 +47,10 @@ export function readHostMarker(env = process.env) {
47
47
  pid: Number(data.pid) || 0,
48
48
  at: Number(data.at) || 0,
49
49
  version: String(data.version || ''),
50
+ // 方案 A:宿主把自己 web server 的监听端口与 drain 令牌一起写进心跳,
51
+ // 独立端据此在**请求上下文**里叫醒宿主应用 pending(旧宿主没这两个字段 → 0/'',走回落)。
52
+ port: Number(data.port) || 0,
53
+ token: String(data.token || ''),
50
54
  }
51
55
  } catch { return null }
52
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.7",
3
+ "version": "0.10.8",
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"
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"
68
68
  },
69
69
  "dependencies": {
70
70
  "@deepseek-ai/schemastery": "^3.18.1"