memory-eternal 0.10.24 → 0.10.26

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/lib/db.js CHANGED
@@ -53,6 +53,38 @@ export function checkDbHealth(db) {
53
53
  return out
54
54
  }
55
55
 
56
+ /** 这个错误是不是「库被别人锁着」(SQLite 的 BUSY / locked),而不是数据或语法问题。 */
57
+ export function isDbBusyError(e) {
58
+ const m = String((e && e.message) || e)
59
+ return /database is locked|database table is locked|SQLITE_BUSY/i.test(m)
60
+ }
61
+
62
+ /**
63
+ * 撞锁时**在应用层异步重试**(issue #31/#33 的实测补充)。
64
+ *
65
+ * 为什么不用 `PRAGMA busy_timeout`:`node:sqlite` 的 `DatabaseSync` 是**同步 API**,
66
+ * 忙等会阻塞宿主的事件循环(整个 DSH 卡住)—— 评审后**故意没做**。这里改成:失败 → 让出事件循环
67
+ * 睡一小会儿 → 再试,最多几次。SQLite 在 BUSY 时不会留下半成品(事务回滚),所以重试是安全的;
68
+ * 实测现场:外部进程持写事务时我方立刻拿到 `database is locked`,且对方被强杀后**仍然锁着** ——
69
+ * 这种瞬时锁正好由短退避重试化解。
70
+ *
71
+ * @param {() => Promise<any>} fn
72
+ * @param {{tries?:number, delayMs?:number}} opts
73
+ */
74
+ export async function withDbRetry(fn, { tries = 3, delayMs = 120 } = {}) {
75
+ const n = Math.max(1, Number(tries) || 3)
76
+ let last
77
+ for (let i = 0; i < n; i++) {
78
+ try { return await fn() } catch (e) {
79
+ last = e
80
+ if (e && e.noRetry) throw e // 调用方显式禁止重试(例如响应已发出)
81
+ if (!isDbBusyError(e) || i === n - 1) throw e
82
+ await new Promise((r) => setTimeout(r, (Number(delayMs) || 120) * (i + 1))) // 线性退避,异步让出
83
+ }
84
+ }
85
+ throw last
86
+ }
87
+
56
88
  /** 某个库最近一次打开时的健康结论(没打开过返回 null)。 */
57
89
  export function dbStatus(root) {
58
90
  return healthByRoot.get(path.resolve(root)) || null
package/lib/watchdog.js CHANGED
@@ -67,12 +67,41 @@ export function readWatchdogLock(env = process.env) {
67
67
  return { watchdogs: [] }
68
68
  }
69
69
 
70
+ let lockWarned = false
71
+
70
72
  function writeWatchdogLock(env, list) {
71
73
  const file = watchdogLockPath(env)
72
74
  try {
73
75
  fs.mkdirSync(path.dirname(file), { recursive: true })
74
76
  fs.writeFileSync(file, JSON.stringify({ watchdogs: list }))
75
- } catch { /* 写不了锁不阻塞保活 */ }
77
+ } catch (e) {
78
+ // 写不了锁**不阻塞保活**,但不能再全静默:这是「托管状态判不出来」的根因(issue #33 第③层,
79
+ // 实测受限环境写不进锁 → 面板显示「未托管」而端口上其实有健康实例)。留一条可查的痕。
80
+ if (!lockWarned) {
81
+ lockWarned = true
82
+ console.error('[memory-eternal] 看门狗锁文件写不进去(' + file + '):' + String((e && e.message) || e) + ' —— 保活不受影响,但「是否被托管」将无法判断')
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 探测「锁文件所在目录现在写不写得了」—— **只探测,不碰锁文件本身**(避免把真实状态写坏)。
89
+ *
90
+ * 用途(issue #33 第③层):面板要区分「**确实没被托管**」与「**判不出来**(受限环境写不进锁)」
91
+ * —— 后者不该被显示成「未托管 / 未运行」,那会让用户以为实例坏了,跑去反复重启。
92
+ */
93
+ export function probeLockWritable(env = process.env) {
94
+ const file = watchdogLockPath(env)
95
+ const probe = path.join(path.dirname(file), '.me-lock-probe-' + process.pid + '-' + Date.now())
96
+ try {
97
+ fs.mkdirSync(path.dirname(file), { recursive: true })
98
+ fs.writeFileSync(probe, 'ok')
99
+ fs.unlinkSync(probe)
100
+ return { writable: true, path: file, error: '' }
101
+ } catch (e) {
102
+ try { fs.unlinkSync(probe) } catch { /* 清不掉也无妨 */ }
103
+ return { writable: false, path: file, error: String((e && e.message) || e) }
104
+ }
76
105
  }
77
106
 
78
107
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.24",
3
+ "version": "0.10.26",
4
4
  "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
5
  "license": "MIT",
6
6
  "repository": {