memory-eternal 0.10.36 → 0.10.37

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memory-eternal",
4
- "version": "0.10.36",
4
+ "version": "0.10.37",
5
5
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
6
6
  "author": {
7
7
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.36",
3
+ "version": "0.10.37",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.36",
3
+ "version": "0.10.37",
4
4
  "description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
5
5
  "author": {
6
6
  "name": "EternalNight996",
package/README.en.md CHANGED
@@ -361,14 +361,15 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
361
361
 
362
362
  | Version | Date | Highlights |
363
363
  |---|---|---|
364
+ | **v0.10.37** | 2026-10-17 | **Fixes #45 (a severe regression I introduced in 0.10.36: the self-probing recursion in /version-check, amplified by retries), #44 (hardcoded node paths / zombie-watchdog misjudgement) and #43 (the official route fields for follow-session)**. ① **#45**: selfPort() is this process own port, yet /version-check called probeServedVersion to **request /version-check from it** — handler to itself to handler, with depth bounded only by timeouts and a new connection per level, which saturated the listen backlog and produced **intermittent ECONNREFUSED (17–37ms)**, 4402 extra TIME_WAIT entries in 40 idle seconds and an empty served value; the 0.10.36 retry multiplied it by three. Self-probes now **answer locally** (probedSelf:true, no HTTP), /restart-self has the same gate, and /version-check accepts ?port= for other ports. ② **#44**: nodeBinary() now **verifies existence** before using a cached path and falls back to PATH when execPath vanished (brew upgrade removing the old directory), so long-lived watchdogs heal themselves; inspectResident gains portServing and decideResidentAction treats "the lock says a watchdog is alive but the port answers nobody" as restart (watchdog-port-dead) instead of delegate; status prints the actual node binary and whether it exists. ③ **#43**: sessionRouteFrom() is async and prefers the official fields, session.requestHeader().config then agent.options, with the old sniffing as a fallback. npm test now runs **375 checks** |
364
365
  | **v0.10.36** | 2026-10-16 | **Both new states now surface at every entry point (align / doctor), plus one test-hygiene fix**. ① align gains a finding: **the resident instance is host-managed** (the lock is unwritable, so the watchdog path is bypassed and the host stops/starts the instance itself and keeps it alive) — **warn level**, stating explicitly that "the instance is managed, it is not an orphan, and it returns to the watchdog once the lock becomes writable" (align.ok is unaffected; warn is not a failure); ② dsh-memory doctor prints a new line **resident instance management:** (watchdog / host-managed / cannot replace (environment limit)), so together with journal mode, write probe, lock writability and the backup list you can see at a glance which state a machine is in; ③ **test hygiene**: the /restart-self alreadyCurrent case depended on the **developer machine real lock file** (my local lock held only an endedAt record, so it stopped reporting alreadyCurrent and the check went red) — reading the lock is now injectable (deps.readWatchdogLock) and the case injects a live registration, making it independent of the dev machine. npm test now runs **371 checks** |
365
- | **v0.10.35** | 2026-10-16 | **Single-flight for takeover helpers (issue #30, sixth retest round: the host tick and the CLI each spawn a --replace and step on each other)**. The reporter observed that during a failed restart **two takeover helpers were alive on the same port without knowing each other** (parent = the DSH host / parent = the CLI): neither wrote the lock and each reaped the other web, making the scene even harder to diagnose. Fix: a new **claim file** (memory-eternal-replace.claim, next to the watchdog lock) — ① **a helper claims it exclusively before starting**; if it cannot, it **exits immediately** (yielding to whoever is already running) and releases it on exit; ② **when restartResident sees a takeover in flight** (holder alive and less than 60s old) it **does not spawn a second helper**, returning stage in-flight with who is running it and how long ago it started; ③ **fail-open**: in restricted environments where the claim cannot be written the takeover still proceeds (permission problems must not block takeover for good), but the reason is surfaced in the result and the log. Tests cover: first-come-first-served / a live holder makes the second yield / stale and dead-pid claims can be taken over / only your own claim is released / the in-flight branch of restartResident. npm test now runs **370 checks** |
366
366
 
367
367
  <details>
368
- <summary>Older release notes (v0.10.34 and earlier · 61 releases, click to expand)</summary>
368
+ <summary>Older release notes (v0.10.35 and earlier · 62 releases, click to expand)</summary>
369
369
 
370
370
  | Version | Date | Highlights |
371
371
  |---|---|---|
372
+ | **v0.10.35** | 2026-10-16 | **Single-flight for takeover helpers (issue #30, sixth retest round: the host tick and the CLI each spawn a --replace and step on each other)**. The reporter observed that during a failed restart **two takeover helpers were alive on the same port without knowing each other** (parent = the DSH host / parent = the CLI): neither wrote the lock and each reaped the other web, making the scene even harder to diagnose. Fix: a new **claim file** (memory-eternal-replace.claim, next to the watchdog lock) — ① **a helper claims it exclusively before starting**; if it cannot, it **exits immediately** (yielding to whoever is already running) and releases it on exit; ② **when restartResident sees a takeover in flight** (holder alive and less than 60s old) it **does not spawn a second helper**, returning stage in-flight with who is running it and how long ago it started; ③ **fail-open**: in restricted environments where the claim cannot be written the takeover still proceeds (permission problems must not block takeover for good), but the reason is surfaced in the result and the log. Tests cover: first-come-first-served / a live holder makes the second yield / stale and dead-pid claims can be taken over / only your own claim is released / the in-flight branch of restartResident. npm test now runs **370 checks** |
372
373
  | **v0.10.34** | 2026-10-16 | **Root fix: when the lock is unwritable the HOST manages the resident instance itself (adopting the reporter's fourth point in issue #30)**. Their diagnosis: on that machine the "registration" happens to live in the only process that cannot write the lock, so automatic replacement is permanently refused. Fixes: ① the host's `autoWebMode=interval` keep-alive loop now probes **the version served on the port**, not just liveness — if it lags and the lock is writable the watchdog/drift path still handles it (the host does not compete); if it lags and the **lock is unwritable** the **host takes over**: `ensureWebServer({forceTakeover:true})` stops only **our own** web, starts a new instance on the **same port** and keeps it alive; ② a testable `takeoverOwnWeb()` with a safety gate that only acts on occupants where `looksLikeOurWeb` is true (foreign processes are never touched); ③ "host-managed" is recorded as state (`recordHostManaged`) so `/version-check` reports `resident.managedBy='host'` and the panel says "**host-managed**" instead of "unmanaged / orphan"; the state clears itself once a watchdog registers again. `npm test` now runs **366 checks** |
373
374
  | **v0.10.33** | 2026-10-15 | **Distillation provider: dynamic dropdowns, best-effort follow-session, first-candidate-only and a test button (first slice of issue #42)**. ① **Dynamic dropdowns**: a new read-only `/llm-routes` endpoint (backed by the host's listProviders/listModels) turns the "distill provider / model" fields from hand-typed IDs into selects (falling back to text inputs when unavailable); ② **Follow the current session** (their top priority): the new `captureFollowSession` config makes host distillation **try** the session's provider/model first (`sessionRouteFrom()` sniffs `agent / session / session.route / session.options`), and **falls back to the explicit config with a log line** when nothing is exposed (DSH does not guarantee a route on the session object — that log is the basis for a future adapter rather than a guess); background/no-session tasks (CLI, watchdog, hooks) can only use the explicit config; ③ **First-candidate-only + test button**: `captureFirstOnly` makes routing take just the first candidate and stop on failure (no more "hit the official provider first, burn a call, then rotate"); `POST /llm-test` makes one small real call and echoes **provider / model / latency / candidate count / error code**, which also verifies ① and ②; ④ also fixes **probe jitter**: `probeServedVersion()` now retries with backoff three times (the reporter measured "version-check 200 at one moment, then status says no service 2s later"), so single-shot probes are no longer fooled by restart windows or transient refusals. `npm test` now runs **363 checks** |
374
375
  | **v0.10.32** | 2026-10-15 | **"The resident instance cannot be replaced" becomes a long-lived state instead of a retry on every boot (issue #30, fifth retest round: the steady state on the A: drive machine)**. The reporter measured 44 "version drift" events over five days, with a mechanism where **every step works as designed yet the instance can never be replaced**: upgrade → the port still serves the previous generation → the host detects drift → it spawns a `--replace` helper → the helper pre-flights and finds `lock.writable=false` → it **correctly refuses** (the 0.10.29 design, zero damage) → the old instance survives → the next boot reports it again. Fixes: ① new `lib/resident-state.js` **records "cannot replace (environment limit)" as state** (cause / lock path / port / versions / count, valid for 7 days); ② the host now **probes lock writability before restarting** and, when unwritable, **stops retrying a doomed replacement**, recording just one line — `warn` the first time, then downgraded to a `boot`-level "known state" on later boots (the reporter's words: they do not want a warn on every boot forever); ③ the wording is explicitly split from "drift": **"the resident instance cannot be replaced (environment limit)"** plus the way out (fix permissions then restart, or run `dsh-memory watchdog --port N` in the foreground); ④ the panel, `/version-check` and `align` all surface the state (`resident.replaceBlocked`; in `align` it is a `warn` finding that clears itself once the environment is fixed). `npm test` now runs **359 checks** |
package/README.md CHANGED
@@ -393,14 +393,15 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
393
393
 
394
394
  | 版本 | 日期 | 关键改动 |
395
395
  |---|---|---|
396
+ | **v0.10.37** | 2026-10-17 | **修 #45(我 0.10.36 引入的严重回归:/version-check 自我探测递归被重试放大)+ #44(node 路径硬编码 / 僵尸看门狗误判)+ #43(跟随会话的官方字段)**。① **#45**:selfPort() 就是本进程自己的端口,而 /version-check 会用 probeServedVersion **再向它发一条 /version-check** → 处理器 → 自己 → 处理器…… 深度只受超时约束、每层各开一条连接 → listen backlog 打满 → **间歇 ECONNREFUSED(17–37ms)**、TIME_WAIT 静置 40 秒涨 4402、served 反成空串;0.10.36 的「重试 3 次」把它**乘以 3**。现在自探一律**直接作答**(probedSelf:true,不发 HTTP),/restart-self 同一道闸门;/version-check 也支持 ?port= 问别的端口。② **#44**:nodeBinary() 缓存命中前**验存在性**,execPath 失效(brew 升级删掉旧目录)时回退 PATH —— 长命 watchdog 从此自愈;inspectResident 新增 portServing,decideResidentAction 遇「登记说 watchdog 活着、端口却没人应答」判 restart(watchdog-port-dead)而**不再 delegate**(那正是「沉淀静默停摆、日志只写 delegated」的现场);status 打印实际用的 node 与存在性。③ **#43**:sessionRouteFrom() 改为 async,按官方字段优先取 session.requestHeader().config → agent.options → 旧嗅探兜底。npm test 共 **375 项** |
396
397
  | **v0.10.36** | 2026-10-16 | **把两种新状态接到所有入口(align / doctor)+ 修一处测试卫生**。① align 新增一条结论:**常驻实例由宿主托管**(锁不可写 → 看门狗路径被绕过,改由宿主停旧起新并保活)—— **warn 级**,明确写「实例是有人管的,不是孤儿;锁恢复可写后自动交还看门狗」(align.ok 不受影响,warn 不算失败);② dsh-memory doctor 增印一行 **常驻实例托管:**(看门狗 / 宿主托管 / 不可替换(环境限制)),配合已有的 journal 模式、写探针、锁可写性、备份清单,一次看清这台机器处于哪种状态;③ **测试卫生**:/restart-self 的 alreadyCurrent 用例依赖**开发者本机真实的锁文件**(我本机锁里只剩一条 endedAt 记录,于是它不再判 alreadyCurrent、用例翻红)—— 把「读锁」做成可注入(deps.readWatchdogLock),用例自己注入一条活登记,从此与开发机状态无关。npm test 共 **371 项** |
397
- | **v0.10.35** | 2026-10-16 | **接管助手单飞互斥(#30 第六轮实测:宿主 tick 与 CLI 各拉一个 --replace 互相踩)**。报告者观察到失败的 restart 期间同端口上**同时存在两个互不相识的接管助手**(父 = DSH 宿主 / 父 = CLI),谁都不写锁、还互相收对方的 web —— 现场更难复盘。修法:新增 **claim 文件**(memory-eternal-replace.claim,与看门狗锁同目录)—— ① **助手开工前独占抢 claim**,抢不到就**直接退出**(把接管让给已经在跑的那个),并在退出时释放;② **restartResident 发现接管进行中**(持有者活着且 <60s)就**不再拉第二个助手**,返回 stage「in-flight」并说明是谁在跑、多久前启动;③ **失败开放**:受限环境写不了 claim 时**照常接管**(不能让权限问题把接管彻底堵死),但把原因写进返回值 / 日志。测试覆盖:先到先得 / 活持有者让位 / 过期与死 pid 可接管 / 只清自己的 claim / restartResident 的 in-flight 分支。npm test 共 **370 项** |
398
398
 
399
399
  <details>
400
- <summary>更早的更新日志(v0.10.34 及以前 · 共 61 版,点开查看)</summary>
400
+ <summary>更早的更新日志(v0.10.35 及以前 · 共 62 版,点开查看)</summary>
401
401
 
402
402
  | 版本 | 日期 | 关键改动 |
403
403
  |---|---|---|
404
+ | **v0.10.35** | 2026-10-16 | **接管助手单飞互斥(#30 第六轮实测:宿主 tick 与 CLI 各拉一个 --replace 互相踩)**。报告者观察到失败的 restart 期间同端口上**同时存在两个互不相识的接管助手**(父 = DSH 宿主 / 父 = CLI),谁都不写锁、还互相收对方的 web —— 现场更难复盘。修法:新增 **claim 文件**(memory-eternal-replace.claim,与看门狗锁同目录)—— ① **助手开工前独占抢 claim**,抢不到就**直接退出**(把接管让给已经在跑的那个),并在退出时释放;② **restartResident 发现接管进行中**(持有者活着且 <60s)就**不再拉第二个助手**,返回 stage「in-flight」并说明是谁在跑、多久前启动;③ **失败开放**:受限环境写不了 claim 时**照常接管**(不能让权限问题把接管彻底堵死),但把原因写进返回值 / 日志。测试覆盖:先到先得 / 活持有者让位 / 过期与死 pid 可接管 / 只清自己的 claim / restartResident 的 in-flight 分支。npm test 共 **370 项** |
404
405
  | **v0.10.34** | 2026-10-16 | **治本:锁不可写时由「宿主」自己托管常驻实例(采纳 #30 报告者第四点建议)**。他的诊断:那台机器上「登记」恰好落在**唯一写不了锁的那个进程**上 → 自动替换被永久拒绝。修法:① 宿主的 `autoWebMode=interval` 保活循环**不再只探存活**,还探**端口上服务的版本** —— 落后且锁可写 → 仍交给看门狗 / 漂移路径(宿主不抢);落后且**锁不可写** → **宿主自己接管**(`ensureWebServer({forceTakeover:true})`:只收掉**自家** web、在**同端口**起新实例、由宿主保活);② 新增可测试的 `takeoverOwnWeb()`(安全闸门:只对 `looksLikeOurWeb` 为真的占用者下手,别人的进程一个都不动);③ 「宿主托管」记成状态(`recordHostManaged`)→ `/version-check` 报 `resident.managedBy='host'`,面板显示「**宿主托管**」而不是「未托管 / 孤儿」;看门狗重新登记后自动清除。`npm test` 共 **366 项** |
405
406
  | **v0.10.33** | 2026-10-15 | **蒸馏供应商:动态下拉 + 跟随当前会话(尽力)+ 仅试首选 + 测试按钮(issue #42 第一刀)**。① **动态下拉**:新增只读接口 `/llm-routes`(宿主侧的 listProviders/listModels),配置页的「蒸馏 provider / model」从**手填编号**变成**下拉选择**(取不到时自动回退成文本输入);② **跟随当前会话**(他的最高优先级):新增配置 `captureFollowSession`,开启后宿主蒸馏**先尝试**用这一轮会话的 provider/model(`sessionRouteFrom()` 按 `agent / session / session.route / session.options` 逐个嗅探),**读不到就回退显式配置并记一条日志**(DSH 未保证会话对象暴露 route —— 这条日志就是以后补适配的依据,不猜);后台 / 无会话任务(CLI、watchdog、hooks)天然只能回退配置;③ **仅试首选 + 测试按钮**:`captureFirstOnly` 让选路只取第一个候选、失败即停(避免「先打官方 → 白烧一次调用 → 再轮换」);`POST /llm-test` 用真实调用路径打一次小请求,回显 **provider / model / 耗时 / 候选数 / 错误码**,也是 ①② 的验证手段。④ 顺带修一处**探测抖动**:`probeServedVersion()` 现在退避重试 3 次(报告者实测「同一时刻 version-check 200、2 秒后 status 说没有服务」),单发探测不再被重启窗口/瞬时拒绝骗到。`npm test` 共 **363 项** |
406
407
  | **v0.10.32** | 2026-10-15 | **「常驻实例不可替换」变成长期状态,而不是每次开机重试(#30 第五轮复测:A: 盘的稳态)**。报告者实测 5 天 44 次「版本漂移」,机制是**每一步都按设计工作、合起来永远换不掉**:升级 → 端口上仍是上一代 → 宿主探测漂移 → 拉起 `--replace` 助手 → 助手前置探到 `lock.writable=false` → **正确地拒绝**(0.10.29 的设计,零破坏)→ 旧实例继续存活 → 下次开机再报一次。修法:① 新增 `lib/resident-state.js`:把「不可替换(环境限制)」**记成状态**(原因 / 锁路径 / 端口 / 版本 / 次数,7 天有效);② 宿主在重启前**先探锁可写性**,不可写就**不再重试注定失败的替换**,只记一条 —— **首次 `warn`**、之后每次开机降级为 `boot` 级「已知状态」(报告者原话:不想每次开机都看到一条 warn 永久持续);③ 措辞与「漂移」显式分开:**「常驻实例不可替换(环境限制)」** + 出路(修权限后 restart,或 `dsh-memory watchdog --port N` 前台观察);④ 面板 / `/version-check` / `align` 都能看到这个状态(`resident.replaceBlocked`;`align` 里是一条 `warn` 级结论,环境修好后自动清除)。`npm test` 共 **359 项** |
@@ -334,6 +334,13 @@ async function main() {
334
334
  console.log('journal_mode:' + (health.journalMode || '(未知)'))
335
335
  console.log('写探针:' + (health.writable ? 'OK(这个库现在可写)' : '失败 → ' + health.error))
336
336
  if (health.corrupt) console.log('损坏判定:疑似文件损坏(见下方引导)')
337
+ // issue #44:长命 watchdog 若用一份**已被删除的** node 绝对路径(brew 升级后很常见),
338
+ // spawn web 每次都 ENOENT 且被静默吞掉 —— 这里把实际用的 node 与存在性直接打出来。
339
+ try {
340
+ const { nodeBinary, binaryUsable } = await import('../lib/node-bin.js')
341
+ const nb = nodeBinary()
342
+ console.log('子进程 node:' + nb + (binaryUsable(nb) ? '' : '(**不存在** → spawn web 会静默失败;请用当前 node 执行 dsh-memory restart)'))
343
+ } catch { /* 取不到就不打印 */ }
337
344
  console.log('看门狗锁:' + (lockInfo.writable ? '可写(' + lockInfo.path + ')' : '**写不进去** → ' + lockInfo.error + '(' + lockInfo.path + ')—— 保活一般仍有效,但「是否被托管」将无法判断'))
338
345
  // 托管方式一眼可见(#30 ⑤/⑥):看门狗登记 / 宿主托管 / 因环境限制不可替换
339
346
  try {
package/index.js CHANGED
@@ -702,7 +702,7 @@ export function apply(ctx, config) {
702
702
  let routeOpts = { provider: cfg.captureProvider, model: cfg.captureModel, firstOnly: cfg.captureFirstOnly === true }
703
703
  if (cfg.captureFollowSession === true) {
704
704
  try {
705
- const sr = sessionRouteFrom(agent)
705
+ const sr = await sessionRouteFrom(agent)
706
706
  if (sr && sr.provider) {
707
707
  routeOpts = { ...routeOpts, provider: sr.provider, model: sr.model || '' }
708
708
  logCapture(sessionId, 'boot', `蒸馏路由跟随当前会话:${sr.provider}${sr.model ? '/' + sr.model : ''}(来源 ${sr.from})`)
@@ -1394,6 +1394,7 @@ export function apply(ctx, config) {
1394
1394
  served: info.served,
1395
1395
  expect: cur,
1396
1396
  watchdogAlive: info.watchdogAlive,
1397
+ portServing: info.portServing,
1397
1398
  occupantIsOurs: info.occupantIsOurs,
1398
1399
  autoRestart: cfg0.autoRestartOnDrift !== false,
1399
1400
  residentStartedAt: info.residentStartedAt,
package/lib/api.js CHANGED
@@ -314,9 +314,15 @@ export function createApi(deps = {}) {
314
314
  // 「常驻 web 旧」——两者要做的操作不同(重启 DSH vs 重启常驻实例)。独立 web 页里
315
315
  // 探到的就是它自己。
316
316
  const cfgVc = getSettings() ?? {}
317
- const residentPort = selfPort() || Number(cfgVc.webPort) || Number(process.env.MEMORY_WEB_PORT) || 7999
317
+ const residentPort = Number(query.get('port')) || selfPort() || Number(cfgVc.webPort) || Number(process.env.MEMORY_WEB_PORT) || 7999
318
+ // issue #45(严重):**绝不能向自己发这条请求**。selfPort() 就是本进程自己的端口,
319
+ // 而 probeServedVersion 打的就是 /version-check —— 于是处理器 → 自己 → 处理器 → ……
320
+ // 深度只受超时约束,每层再开一条连接:listen backlog 被打满 → 间歇 ECONNREFUSED(17–37ms)、
321
+ // TIME_WAIT 静置也在涨、served 反而成了空串;0.10.36 的「重试 3 次」把它又乘以 3。
322
+ const probedSelf = Number(residentPort) === Number(selfPort())
318
323
  let residentServed = ''
319
- try { residentServed = String((await probeResidentVersion(residentPort)) || '') } catch { residentServed = '' }
324
+ if (probedSelf) residentServed = String(loaded || '')
325
+ else { try { residentServed = String((await probeResidentVersion(residentPort)) || '') } catch { residentServed = '' } }
320
326
  // 「在服务」≠「被托管」:web 可能是宿主 ensureWeb() 直接拉起的、或看门狗被 stop/kill 后残留的孤儿
321
327
  // —— 此时锁里没有活着的看门狗,端口上的实例没人保活、也不会随版本升级被替换。
322
328
  // #28 的现场就是这一种:host 与端口版本号一致 → 面板认为「无需重启」,而其实该重启(重新托管)。
@@ -351,6 +357,7 @@ export function createApi(deps = {}) {
351
357
  managed: residentManaged,
352
358
  managedKnown,
353
359
  managedBy,
360
+ probedSelf: probedSelf === true,
354
361
  hostManaged,
355
362
  pid: process.pid,
356
363
  lock: lockInfo,
@@ -377,7 +384,11 @@ export function createApi(deps = {}) {
377
384
  const cfgR = getSettings() ?? {}
378
385
  const port = Number(query.get('port')) || selfPort() || Number(cfgR.webPort) || Number(process.env.MEMORY_WEB_PORT) || 7999
379
386
  const expect = pkgVersion()
380
- const served = await probeResidentVersion(port).catch(() => '')
387
+ // 与 /version-check 同一条闸门(#45):问自己等于开一条递归链 —— 本进程就是那个常驻实例,
388
+ // 直接用自己的版本作答。
389
+ const served = Number(port) === Number(selfPort())
390
+ ? String(loaded || expect || '')
391
+ : await probeResidentVersion(port).catch(() => '')
381
392
  // 「版本一致」还不够:还要**有活着的看门狗托管它**。孤儿 web(看门狗被 stop / 被 kill -9 后残留)
382
393
  // 版本号可能正好也是新的,但没人保活、也不会被后续升级替换 —— 那时一键重启才是唯一出路(#28)。
383
394
  const managed = (() => {
package/lib/capture.js CHANGED
@@ -837,8 +837,34 @@ export function compressExcerpt(text, maxChars = 2400) {
837
837
  *
838
838
  * @returns {{provider:string, model:string, from:string}|null}
839
839
  */
840
- export function sessionRouteFrom(agent) {
840
+ export async function sessionRouteFrom(agent) {
841
841
  if (!agent || typeof agent !== 'object') return null
842
+ const s0 = agent.session
843
+ const pick = (obj, pKeys, mKeys) => {
844
+ if (!obj || typeof obj !== 'object') return null
845
+ for (const pk of pKeys) {
846
+ const pv = obj[pk]
847
+ if (typeof pv === 'string' && pv.trim()) {
848
+ let model = ''
849
+ for (const mk of mKeys) { const mv = obj[mk]; if (typeof mv === 'string' && mv.trim()) { model = mv.trim(); break } }
850
+ return { provider: pv.trim(), model }
851
+ }
852
+ }
853
+ return null
854
+ }
855
+ // ① 官方同款(issue #43 定位):session.requestHeader()?.config —— 折叠后的每轮请求配置。
856
+ // 官方 dsh-tool-fs 就这么取;它是**最省事且最可靠**的一处。
857
+ try {
858
+ const h = s0 && typeof s0.requestHeader === 'function' ? await s0.requestHeader() : null
859
+ const cfg = h && (h.config || h)
860
+ const got = pick(cfg, ['providerId', 'provider'], ['modelId', 'model'])
861
+ if (got) return { ...got, from: 'session.requestHeader().config' }
862
+ } catch { /* 继续往下 */ }
863
+ // ② 官方同源:agent.options.provider / .model(系统提示词变量也是取它)—— 我先前漏掉的位置。
864
+ {
865
+ const got = pick(agent.options, ['providerId', 'provider'], ['modelId', 'model'])
866
+ if (got) return { ...got, from: 'agent.options' }
867
+ }
842
868
  const cands = []
843
869
  const s = agent.session
844
870
  const push = (obj, from, pKeys, mKeys) => {
package/lib/client.js CHANGED
@@ -298,7 +298,7 @@ Option A, because:
298
298
  height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
299
299
  max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
300
300
  }
301
- `;function Ca(t){t.effect(()=>t.locale.register(st,{zh:Jt,en:Zt}),"memory-eternal: locale");let i=t.locale.bind(st);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:st,order:25,label:()=>i("nav"),locale:st,inject:()=>({})},()=>o.default.createElement(Ma,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${st}:footer`,order:100,label:()=>i("nav"),locale:st,inject:()=>({})},r=>o.default.createElement(Ra,{t:i,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Na(){let[t,i]=(0,o.useState)(O+"/ui/app");return(0,o.useEffect)(()=>{let r=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&i(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch(O+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return g()}).catch(()=>r?g():void 0),()=>{r=!1}},[]),t}function Ma({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)(aa,{t,onReload:()=>{},version:0})]})}function Ra({t,locale:i,wide:r}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(ea,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(Ta,{t,locale:i,onClose:()=>c(!1)})]})}function Ta({t,locale:i,onClose:r,tab:g}){let[c,l]=(0,o.useState)(!1),C=(0,o.useRef)(null),T=Na(),S=(0,o.useCallback)(R=>i&&typeof i.subscribe=="function"?i.subscribe(R):()=>{},[i]),H=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),k=(0,o.useSyncExternalStore)(S,H),x=k&&(k.active==="zh"||k.active==="en")?k.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let R=m.current?`lang=${m.current}`:"";if(g==="config")return`${O}/ui/config?tab=config${R?`&${R}`:""}`;let w=T.replace(/\/$/,""),j=w.endsWith("/ui/app")?"?":"/?";return g?`${w}${j}tab=${g}${R?`&${R}`:""}`:R?`${w}${j}${R}`:T},[T,g]);return(0,o.useEffect)(()=>{let R=C.current&&C.current.contentWindow;R&&x&&R.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let R=w=>{w.key==="Escape"&&r()};return window.addEventListener("keydown",R),()=>window.removeEventListener("keydown",R)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:R=>R.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>l(R=>!R),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:r,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:C,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function ea(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var yt="0.10.36";function Da({t,inModal:i,onClose:r,onFull:g,full:c}){let[l,C]=(0,o.useState)(null),[T,S]=(0,o.useState)([]),[H,k]=(0,o.useState)(!0),[x,m]=(0,o.useState)(null),[z,R]=(0,o.useState)(""),[w,j]=(0,o.useState)("all"),[L,K]=(0,o.useState)(""),[te,V]=(0,o.useState)("all"),[F,Ie]=(0,o.useState)("all"),[G,ge]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),n={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&n[b])return n[b]}catch{}return"cards"}),[X,$]=(0,o.useState)(()=>new Set([G])),le=(0,o.useCallback)(b=>{$(n=>n.has(b)?n:new Set(n).add(b)),ge(b)},[]),[he,Ae]=(0,o.useState)("recent"),[ke,y]=(0,o.useState)(null),ue=(0,o.useRef)(null),Te=(0,o.useRef)(null),[h,U]=(0,o.useState)(!1),[re,ye]=(0,o.useState)(null),[ee,Z]=(0,o.useState)(!1),[Be,Ce]=(0,o.useState)(!1),[Re,Ne]=(0,o.useState)(!0),[s,N]=(0,o.useState)(null),[I,Q]=(0,o.useState)(0),we=(0,o.useCallback)(()=>Q(b=>b+1),[]),de=l?.quarantine?.pending??0,ae=l?.quarantine?.deleted??0,Y=(0,o.useRef)(null),[We,Le]=(0,o.useState)(0),[p,d]=(0,o.useState)(!1),_=(0,o.useRef)(null),se=(0,o.useRef)(null),ce=(0,o.useCallback)(async(b=w,n=L,v=F,D={})=>{let B=D.append===!0;try{B?d(!0):(k(!0),R(""));let E=new URLSearchParams;b&&b!=="all"&&E.set("kind",b),n.trim()&&E.set("q",n.trim()),E.set("status",v||"all"),E.set("limit",String(xa)),E.set("offset",String(D.offset||0)),he==="title"&&E.set("sort","title");let J=await(await fetch(`${O}/cards?${E.toString()}`)).json();if(J.ok){let ve=J.cards||[];S(De=>B?[...De,...ve.filter(Pe=>!De.some(vt=>vt.path===Pe.path))]:ve),Le(Number(J.total)||ve.length)}}catch(E){R(String(E&&E.message?E.message:E))}finally{B?d(!1):k(!1)}},[w,L,F,he]),fe=(0,o.useCallback)(async()=>{try{let[b,n]=await Promise.all([fetch(`${O}/overview`).then(v=>v.json()),fetch(`${O}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&C(b),m(n&&n.ok&&n.available&&n.health||null)}catch(b){R(String(b&&b.message?b.message:b))}finally{k(!1)}},[]),a=async b=>{try{let v=await(await fetch(`${O}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(y({ok:!0,msg:t("deleted")}),await fe(),we()):y({ok:!1,msg:v.error||t("deleteFail")})}catch{y({ok:!1,msg:t("deleteFail")})}ue.current&&clearTimeout(ue.current),ue.current=setTimeout(()=>y(null),2e3)};(0,o.useEffect)(()=>(fe(),()=>{Y.current&&clearTimeout(Y.current)}),[fe]);let Fe=b=>{K(b),Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>ce(w,b,F),280)},Je=b=>{j(b),ce(b,L,F)};(0,o.useEffect)(()=>{G==="cards"&&ce(w,L,F)},[F,G,I,he]),(0,o.useEffect)(()=>{if(G!=="cards"||H||p||T.length>=We)return;let b=_.current,n=se.current;if(!b)return;let v=new IntersectionObserver(D=>{D[0]&&D[0].isIntersecting&&ce(w,L,F,{append:!0,offset:T.length})},{root:n||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[G,T,We,H,p,w,L,F,ce]);let et=async b=>{try{let n=b.status;if(!n&&b.path)try{let W=((await fetch(`${O}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(J=>J.json())).cards||[]).find(J=>J.path===b.path);W&&(n=W.status)}catch{}let v=n&&n!=="approved"?`&status=${encodeURIComponent(n)}`:"",D=await fetch(`${O}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),B=await D.json();B.ok?ye({path:B.path,title:b.title,text:B.text}):R(B.error||`HTTP ${D.status}`)}catch(n){R(String(n&&n.message?n.message:n))}},at=async(b,n)=>{U(!0);try{let v=await fetch(`${O}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let D=await v.json();if(!D.ok)throw new Error(D.error||"export failed");let B,E,W;if(b==="json"){E="memory-vault.json",W="application/json";let De=D.cards.map(Pe=>({path:Pe.path,title:Pe.title,kind:Pe.kind,status:Pe.status,store:Pe.store,text:Pe.text}));B=JSON.stringify({format:D.format||"memory-eternal-vault",formatVersion:D.formatVersion||1,exportedAt:D.exportedAt||new Date().toISOString(),count:De.length,cards:De},null,2)}else E="memory-vault.md",W="text/markdown;charset=utf-8",B=D.cards.map(De=>De.text.trim()).filter(Boolean).join(`
301
+ `;function Ca(t){t.effect(()=>t.locale.register(st,{zh:Jt,en:Zt}),"memory-eternal: locale");let i=t.locale.bind(st);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:st,order:25,label:()=>i("nav"),locale:st,inject:()=>({})},()=>o.default.createElement(Ma,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${st}:footer`,order:100,label:()=>i("nav"),locale:st,inject:()=>({})},r=>o.default.createElement(Ra,{t:i,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Na(){let[t,i]=(0,o.useState)(O+"/ui/app");return(0,o.useEffect)(()=>{let r=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&i(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch(O+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return g()}).catch(()=>r?g():void 0),()=>{r=!1}},[]),t}function Ma({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)(aa,{t,onReload:()=>{},version:0})]})}function Ra({t,locale:i,wide:r}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(ea,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(Ta,{t,locale:i,onClose:()=>c(!1)})]})}function Ta({t,locale:i,onClose:r,tab:g}){let[c,l]=(0,o.useState)(!1),C=(0,o.useRef)(null),T=Na(),S=(0,o.useCallback)(R=>i&&typeof i.subscribe=="function"?i.subscribe(R):()=>{},[i]),H=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),k=(0,o.useSyncExternalStore)(S,H),x=k&&(k.active==="zh"||k.active==="en")?k.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let R=m.current?`lang=${m.current}`:"";if(g==="config")return`${O}/ui/config?tab=config${R?`&${R}`:""}`;let w=T.replace(/\/$/,""),j=w.endsWith("/ui/app")?"?":"/?";return g?`${w}${j}tab=${g}${R?`&${R}`:""}`:R?`${w}${j}${R}`:T},[T,g]);return(0,o.useEffect)(()=>{let R=C.current&&C.current.contentWindow;R&&x&&R.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let R=w=>{w.key==="Escape"&&r()};return window.addEventListener("keydown",R),()=>window.removeEventListener("keydown",R)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:R=>R.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>l(R=>!R),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:r,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:C,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function ea(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var yt="0.10.37";function Da({t,inModal:i,onClose:r,onFull:g,full:c}){let[l,C]=(0,o.useState)(null),[T,S]=(0,o.useState)([]),[H,k]=(0,o.useState)(!0),[x,m]=(0,o.useState)(null),[z,R]=(0,o.useState)(""),[w,j]=(0,o.useState)("all"),[L,K]=(0,o.useState)(""),[te,V]=(0,o.useState)("all"),[F,Ie]=(0,o.useState)("all"),[G,ge]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),n={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&n[b])return n[b]}catch{}return"cards"}),[X,$]=(0,o.useState)(()=>new Set([G])),le=(0,o.useCallback)(b=>{$(n=>n.has(b)?n:new Set(n).add(b)),ge(b)},[]),[he,Ae]=(0,o.useState)("recent"),[ke,y]=(0,o.useState)(null),ue=(0,o.useRef)(null),Te=(0,o.useRef)(null),[h,U]=(0,o.useState)(!1),[re,ye]=(0,o.useState)(null),[ee,Z]=(0,o.useState)(!1),[Be,Ce]=(0,o.useState)(!1),[Re,Ne]=(0,o.useState)(!0),[s,N]=(0,o.useState)(null),[I,Q]=(0,o.useState)(0),we=(0,o.useCallback)(()=>Q(b=>b+1),[]),de=l?.quarantine?.pending??0,ae=l?.quarantine?.deleted??0,Y=(0,o.useRef)(null),[We,Le]=(0,o.useState)(0),[p,d]=(0,o.useState)(!1),_=(0,o.useRef)(null),se=(0,o.useRef)(null),ce=(0,o.useCallback)(async(b=w,n=L,v=F,D={})=>{let B=D.append===!0;try{B?d(!0):(k(!0),R(""));let E=new URLSearchParams;b&&b!=="all"&&E.set("kind",b),n.trim()&&E.set("q",n.trim()),E.set("status",v||"all"),E.set("limit",String(xa)),E.set("offset",String(D.offset||0)),he==="title"&&E.set("sort","title");let J=await(await fetch(`${O}/cards?${E.toString()}`)).json();if(J.ok){let ve=J.cards||[];S(De=>B?[...De,...ve.filter(Pe=>!De.some(vt=>vt.path===Pe.path))]:ve),Le(Number(J.total)||ve.length)}}catch(E){R(String(E&&E.message?E.message:E))}finally{B?d(!1):k(!1)}},[w,L,F,he]),fe=(0,o.useCallback)(async()=>{try{let[b,n]=await Promise.all([fetch(`${O}/overview`).then(v=>v.json()),fetch(`${O}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&C(b),m(n&&n.ok&&n.available&&n.health||null)}catch(b){R(String(b&&b.message?b.message:b))}finally{k(!1)}},[]),a=async b=>{try{let v=await(await fetch(`${O}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(y({ok:!0,msg:t("deleted")}),await fe(),we()):y({ok:!1,msg:v.error||t("deleteFail")})}catch{y({ok:!1,msg:t("deleteFail")})}ue.current&&clearTimeout(ue.current),ue.current=setTimeout(()=>y(null),2e3)};(0,o.useEffect)(()=>(fe(),()=>{Y.current&&clearTimeout(Y.current)}),[fe]);let Fe=b=>{K(b),Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>ce(w,b,F),280)},Je=b=>{j(b),ce(b,L,F)};(0,o.useEffect)(()=>{G==="cards"&&ce(w,L,F)},[F,G,I,he]),(0,o.useEffect)(()=>{if(G!=="cards"||H||p||T.length>=We)return;let b=_.current,n=se.current;if(!b)return;let v=new IntersectionObserver(D=>{D[0]&&D[0].isIntersecting&&ce(w,L,F,{append:!0,offset:T.length})},{root:n||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[G,T,We,H,p,w,L,F,ce]);let et=async b=>{try{let n=b.status;if(!n&&b.path)try{let W=((await fetch(`${O}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(J=>J.json())).cards||[]).find(J=>J.path===b.path);W&&(n=W.status)}catch{}let v=n&&n!=="approved"?`&status=${encodeURIComponent(n)}`:"",D=await fetch(`${O}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),B=await D.json();B.ok?ye({path:B.path,title:b.title,text:B.text}):R(B.error||`HTTP ${D.status}`)}catch(n){R(String(n&&n.message?n.message:n))}},at=async(b,n)=>{U(!0);try{let v=await fetch(`${O}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let D=await v.json();if(!D.ok)throw new Error(D.error||"export failed");let B,E,W;if(b==="json"){E="memory-vault.json",W="application/json";let De=D.cards.map(Pe=>({path:Pe.path,title:Pe.title,kind:Pe.kind,status:Pe.status,store:Pe.store,text:Pe.text}));B=JSON.stringify({format:D.format||"memory-eternal-vault",formatVersion:D.formatVersion||1,exportedAt:D.exportedAt||new Date().toISOString(),count:De.length,cards:De},null,2)}else E="memory-vault.md",W="text/markdown;charset=utf-8",B=D.cards.map(De=>De.text.trim()).filter(Boolean).join(`
302
302
 
303
303
  ---
304
304
 
package/lib/node-bin.js CHANGED
@@ -44,18 +44,49 @@ function findNodeOnPath() {
44
44
  * 返回用于 spawn 子进程(跑 .js 脚本)的可执行文件路径。
45
45
  * @returns {string}
46
46
  */
47
+ function exists(p) {
48
+ try { return Boolean(p) && fs.statSync(p).isFile() } catch { return false }
49
+ }
50
+
51
+ /** 该候选路径现在还能用吗(issue #44:Homebrew 升级会把旧 node 目录删掉)。 */
52
+ export function binaryUsable(p) { return exists(p) }
53
+
47
54
  export function nodeBinary() {
55
+ // issue #44(macOS/Homebrew):watchdog 是长命进程,它启动时用的是当时的 node 绝对路径。
56
+ // `brew upgrade node` 之后旧目录被删,长命进程的 process.execPath 就指向**已不存在的路径** →
57
+ // 每次 spawn web 都 ENOENT,而调用方是静默吞错的 —— 端口没人应答、锁里却写着「运行中」。
58
+ // 所以缓存命中前先**验存在**:失效就重新解析(PATH 里通常已经是新 node)。
59
+ if (cachedBinary && cachedSource !== 'env' && !exists(cachedBinary)) {
60
+ cachedBinary = null
61
+ cachedSource = null
62
+ }
48
63
  if (cachedBinary) return cachedBinary
49
64
 
50
65
  if (process.env.MEMORY_ETERNAL_NODE) {
51
- cachedBinary = process.env.MEMORY_ETERNAL_NODE
52
- cachedSource = 'env'
53
- return cachedBinary
66
+ // 显式指定的逃生口优先,但也要求它存在(否则会静默 spwan 失败)
67
+ if (exists(process.env.MEMORY_ETERNAL_NODE) || process.versions.electron) {
68
+ cachedBinary = process.env.MEMORY_ETERNAL_NODE
69
+ cachedSource = 'env'
70
+ return cachedBinary
71
+ }
54
72
  }
55
73
 
56
74
  if (!process.versions.electron) {
75
+ // 非 Electron:正常情况下 process.execPath 就是 node 本身;但**长命进程**(watchdog)的
76
+ // execPath 指向的是它启动时那份 node —— brew 升级后可能已被删除(#44)。此时退回 PATH。
77
+ if (exists(process.execPath)) {
78
+ cachedBinary = process.execPath
79
+ cachedSource = 'execPath'
80
+ return cachedBinary
81
+ }
82
+ const viaPath = findNodeOnPath()
83
+ if (viaPath) {
84
+ cachedBinary = viaPath
85
+ cachedSource = 'path-fallback'
86
+ return cachedBinary
87
+ }
57
88
  cachedBinary = process.execPath
58
- cachedSource = 'execPath'
89
+ cachedSource = 'execPath-missing'
59
90
  return cachedBinary
60
91
  }
61
92
 
package/lib/watchdog.js CHANGED
@@ -719,9 +719,16 @@ export async function inspectResident({ env = process.env, port, probeVersion, f
719
719
  try { occupant = await (findListener || findPortListener)(p) } catch { occupant = null }
720
720
  }
721
721
  const occupantIsOurs = Boolean(occupant && occupant.pid && looksLikeOurWeb(occupant.command))
722
+ // issue #44 建议 3:锁里「有活着的 watchdog」不等于端口有人应答 —— 例如 brew 升级 node 后
723
+ // 那个 watchdog 还在、但它 spawn web 每次都 ENOENT(路径已删)。决策前必须单独问一次端口。
724
+ let portServing = null
725
+ if (p) {
726
+ try { portServing = Boolean(served) || Boolean(await probeTcp(p)) } catch { portServing = served ? true : null }
727
+ }
722
728
  return {
723
729
  port: p,
724
730
  served,
731
+ portServing,
725
732
  occupant,
726
733
  occupantIsOurs,
727
734
  watchdog: entry,
@@ -745,13 +752,20 @@ export async function inspectResident({ env = process.env, port, probeVersion, f
745
752
  * @returns {{action:'delegate'|'spawn'|'restart'|'warn', stale:boolean, reason:string}}
746
753
  */
747
754
  export function decideResidentAction({
748
- served = '', expect = '', watchdogAlive = false, occupantIsOurs = false,
755
+ served = '', expect = '', watchdogAlive = false, occupantIsOurs = false, portServing = null,
749
756
  autoRestart = true, residentStartedAt = 0, now = Date.now(), debounceMs = 60000,
750
757
  } = {}) {
751
758
  const s = String(served || '')
752
759
  const e = String(expect || '')
753
760
  // ① 版本一致:端口上跑的就是本机这份代码
754
761
  if (s && e && s === e) return { action: 'delegate', stale: false, reason: 'up-to-date' }
762
+ // ①b 「登记说活着、端口却没人应答」(#44):这是**僵尸看门狗**(典型原因:brew 升级 node 后
763
+ // 它 spawn web 每次都 ENOENT,而调用方静默吞错)。绝不能 delegate —— 那正是「记忆自动沉淀
764
+ // 静默停摆、日志里只写 delegated」的现场。交给重启流程,由助手用当前 node 重新拉起。
765
+ if (!s && watchdogAlive && portServing === false) {
766
+ if (!autoRestart) return { action: 'warn', stale: true, reason: 'watchdog-port-dead' }
767
+ return { action: 'restart', stale: true, reason: 'watchdog-port-dead' }
768
+ }
755
769
  // ② 读不到本机版本 → 没有判据,绝不乱动常驻实例(宁可不换,也不误杀)
756
770
  if (!e) {
757
771
  if (s) return { action: 'delegate', stale: false, reason: 'expect-unknown' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.36",
3
+ "version": "0.10.37",
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 && node tests/host-managed.test.mjs && node tests/replace-claim.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 && node tests/version-check-self.test.mjs"
68
68
  },
69
69
  "dependencies": {
70
70
  "@deepseek-ai/schemastery": "^3.18.1"
package/web/app.js CHANGED
@@ -300,7 +300,7 @@ Option A, because:
300
300
  height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
301
301
  max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
302
302
  }
303
- `;function Kg(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,i.jsx)("defs",{children:(0,i.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,i.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,i.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,i.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,i.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,i.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var Ko="0.10.36";function tf({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,x.useState)(null),[s,u]=(0,x.useState)([]),[p,h]=(0,x.useState)(!0),[v,m]=(0,x.useState)(null),[C,M]=(0,x.useState)(""),[b,j]=(0,x.useState)("all"),[d,c]=(0,x.useState)(""),[y,S]=(0,x.useState)("all"),[E,H]=(0,x.useState)("all"),[D,A]=(0,x.useState)(()=>{try{let P=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),g={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(P&&g[P])return g[P]}catch{}return"cards"}),[Q,F]=(0,x.useState)(()=>new Set([D])),ne=(0,x.useCallback)(P=>{F(g=>g.has(P)?g:new Set(g).add(P)),A(P)},[]),[ke,it]=(0,x.useState)("recent"),[Le,R]=(0,x.useState)(null),xe=(0,x.useRef)(null),Qe=(0,x.useRef)(null),[L,Z]=(0,x.useState)(!1),[pe,Ne]=(0,x.useState)(null),[ie,oe]=(0,x.useState)(!1),[at,Ae]=(0,x.useState)(!1),[$e,He]=(0,x.useState)(!0),[w,O]=(0,x.useState)(null),[U,le]=(0,x.useState)(0),Fe=(0,x.useCallback)(()=>le(P=>P+1),[]),ge=l?.quarantine?.pending??0,se=l?.quarantine?.deleted??0,te=(0,x.useRef)(null),[Tt,pt]=(0,x.useState)(0),[N,k]=(0,x.useState)(!1),J=(0,x.useRef)(null),fe=(0,x.useRef)(null),ye=(0,x.useCallback)(async(P=b,g=d,_=E,W={})=>{let K=W.append===!0;try{K?k(!0):(h(!0),M(""));let G=new URLSearchParams;P&&P!=="all"&&G.set("kind",P),g.trim()&&G.set("q",g.trim()),G.set("status",_||"all"),G.set("limit",String(Ug)),G.set("offset",String(W.offset||0)),ke==="title"&&G.set("sort","title");let re=await(await fetch(`${q}/cards?${G.toString()}`)).json();if(re.ok){let Te=re.cards||[];u(Xe=>K?[...Xe,...Te.filter(wt=>!Xe.some(Go=>Go.path===wt.path))]:Te),pt(Number(re.total)||Te.length)}}catch(G){M(String(G&&G.message?G.message:G))}finally{K?k(!1):h(!1)}},[b,d,E,ke]),be=(0,x.useCallback)(async()=>{try{let[P,g]=await Promise.all([fetch(`${q}/overview`).then(_=>_.json()),fetch(`${q}/capture-log`).then(_=>_.json()).catch(()=>null)]);P.ok&&a(P),m(g&&g.ok&&g.available&&g.health||null)}catch(P){M(String(P&&P.message?P.message:P))}finally{h(!1)}},[]),f=async P=>{try{let _=await(await fetch(`${q}/delete?path=${encodeURIComponent(P)}`)).json();_.ok?(R({ok:!0,msg:e("deleted")}),await be(),Fe()):R({ok:!1,msg:_.error||e("deleteFail")})}catch{R({ok:!1,msg:e("deleteFail")})}xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>R(null),2e3)};(0,x.useEffect)(()=>(be(),()=>{te.current&&clearTimeout(te.current)}),[be]);let xt=P=>{c(P),te.current&&clearTimeout(te.current),te.current=setTimeout(()=>ye(b,P,E),280)},sn=P=>{j(P),ye(P,d,E)};(0,x.useEffect)(()=>{D==="cards"&&ye(b,d,E)},[E,D,U,ke]),(0,x.useEffect)(()=>{if(D!=="cards"||p||N||s.length>=Tt)return;let P=J.current,g=fe.current;if(!P)return;let _=new IntersectionObserver(W=>{W[0]&&W[0].isIntersecting&&ye(b,d,E,{append:!0,offset:s.length})},{root:g||null,rootMargin:"400px"});return _.observe(P),()=>_.disconnect()},[D,s,Tt,p,N,b,d,E,ye]);let bn=async P=>{try{let g=P.status;if(!g&&P.path)try{let X=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(P.title||"")}&limit=50`).then(re=>re.json())).cards||[]).find(re=>re.path===P.path);X&&(g=X.status)}catch{}let _=g&&g!=="approved"?`&status=${encodeURIComponent(g)}`:"",W=await fetch(`${q}/card?path=${encodeURIComponent(P.path||P.id)}${_}`),K=await W.json();K.ok?Ne({path:K.path,title:P.title,text:K.text}):M(K.error||`HTTP ${W.status}`)}catch(g){M(String(g&&g.message?g.message:g))}},Vn=async(P,g)=>{Z(!0);try{let _=await fetch(`${q}/export`);if(!_.ok)throw new Error("HTTP "+_.status);let W=await _.json();if(!W.ok)throw new Error(W.error||"export failed");let K,G,X;if(P==="json"){G="memory-vault.json",X="application/json";let Xe=W.cards.map(wt=>({path:wt.path,title:wt.title,kind:wt.kind,status:wt.status,store:wt.store,text:wt.text}));K=JSON.stringify({format:W.format||"memory-eternal-vault",formatVersion:W.formatVersion||1,exportedAt:W.exportedAt||new Date().toISOString(),count:Xe.length,cards:Xe},null,2)}else G="memory-vault.md",X="text/markdown;charset=utf-8",K=W.cards.map(Xe=>Xe.text.trim()).filter(Boolean).join(`
303
+ `;function Kg(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,i.jsx)("defs",{children:(0,i.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,i.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,i.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,i.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,i.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,i.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,i.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var Ko="0.10.37";function tf({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,x.useState)(null),[s,u]=(0,x.useState)([]),[p,h]=(0,x.useState)(!0),[v,m]=(0,x.useState)(null),[C,M]=(0,x.useState)(""),[b,j]=(0,x.useState)("all"),[d,c]=(0,x.useState)(""),[y,S]=(0,x.useState)("all"),[E,H]=(0,x.useState)("all"),[D,A]=(0,x.useState)(()=>{try{let P=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),g={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(P&&g[P])return g[P]}catch{}return"cards"}),[Q,F]=(0,x.useState)(()=>new Set([D])),ne=(0,x.useCallback)(P=>{F(g=>g.has(P)?g:new Set(g).add(P)),A(P)},[]),[ke,it]=(0,x.useState)("recent"),[Le,R]=(0,x.useState)(null),xe=(0,x.useRef)(null),Qe=(0,x.useRef)(null),[L,Z]=(0,x.useState)(!1),[pe,Ne]=(0,x.useState)(null),[ie,oe]=(0,x.useState)(!1),[at,Ae]=(0,x.useState)(!1),[$e,He]=(0,x.useState)(!0),[w,O]=(0,x.useState)(null),[U,le]=(0,x.useState)(0),Fe=(0,x.useCallback)(()=>le(P=>P+1),[]),ge=l?.quarantine?.pending??0,se=l?.quarantine?.deleted??0,te=(0,x.useRef)(null),[Tt,pt]=(0,x.useState)(0),[N,k]=(0,x.useState)(!1),J=(0,x.useRef)(null),fe=(0,x.useRef)(null),ye=(0,x.useCallback)(async(P=b,g=d,_=E,W={})=>{let K=W.append===!0;try{K?k(!0):(h(!0),M(""));let G=new URLSearchParams;P&&P!=="all"&&G.set("kind",P),g.trim()&&G.set("q",g.trim()),G.set("status",_||"all"),G.set("limit",String(Ug)),G.set("offset",String(W.offset||0)),ke==="title"&&G.set("sort","title");let re=await(await fetch(`${q}/cards?${G.toString()}`)).json();if(re.ok){let Te=re.cards||[];u(Xe=>K?[...Xe,...Te.filter(wt=>!Xe.some(Go=>Go.path===wt.path))]:Te),pt(Number(re.total)||Te.length)}}catch(G){M(String(G&&G.message?G.message:G))}finally{K?k(!1):h(!1)}},[b,d,E,ke]),be=(0,x.useCallback)(async()=>{try{let[P,g]=await Promise.all([fetch(`${q}/overview`).then(_=>_.json()),fetch(`${q}/capture-log`).then(_=>_.json()).catch(()=>null)]);P.ok&&a(P),m(g&&g.ok&&g.available&&g.health||null)}catch(P){M(String(P&&P.message?P.message:P))}finally{h(!1)}},[]),f=async P=>{try{let _=await(await fetch(`${q}/delete?path=${encodeURIComponent(P)}`)).json();_.ok?(R({ok:!0,msg:e("deleted")}),await be(),Fe()):R({ok:!1,msg:_.error||e("deleteFail")})}catch{R({ok:!1,msg:e("deleteFail")})}xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>R(null),2e3)};(0,x.useEffect)(()=>(be(),()=>{te.current&&clearTimeout(te.current)}),[be]);let xt=P=>{c(P),te.current&&clearTimeout(te.current),te.current=setTimeout(()=>ye(b,P,E),280)},sn=P=>{j(P),ye(P,d,E)};(0,x.useEffect)(()=>{D==="cards"&&ye(b,d,E)},[E,D,U,ke]),(0,x.useEffect)(()=>{if(D!=="cards"||p||N||s.length>=Tt)return;let P=J.current,g=fe.current;if(!P)return;let _=new IntersectionObserver(W=>{W[0]&&W[0].isIntersecting&&ye(b,d,E,{append:!0,offset:s.length})},{root:g||null,rootMargin:"400px"});return _.observe(P),()=>_.disconnect()},[D,s,Tt,p,N,b,d,E,ye]);let bn=async P=>{try{let g=P.status;if(!g&&P.path)try{let X=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(P.title||"")}&limit=50`).then(re=>re.json())).cards||[]).find(re=>re.path===P.path);X&&(g=X.status)}catch{}let _=g&&g!=="approved"?`&status=${encodeURIComponent(g)}`:"",W=await fetch(`${q}/card?path=${encodeURIComponent(P.path||P.id)}${_}`),K=await W.json();K.ok?Ne({path:K.path,title:P.title,text:K.text}):M(K.error||`HTTP ${W.status}`)}catch(g){M(String(g&&g.message?g.message:g))}},Vn=async(P,g)=>{Z(!0);try{let _=await fetch(`${q}/export`);if(!_.ok)throw new Error("HTTP "+_.status);let W=await _.json();if(!W.ok)throw new Error(W.error||"export failed");let K,G,X;if(P==="json"){G="memory-vault.json",X="application/json";let Xe=W.cards.map(wt=>({path:wt.path,title:wt.title,kind:wt.kind,status:wt.status,store:wt.store,text:wt.text}));K=JSON.stringify({format:W.format||"memory-eternal-vault",formatVersion:W.formatVersion||1,exportedAt:W.exportedAt||new Date().toISOString(),count:Xe.length,cards:Xe},null,2)}else G="memory-vault.md",X="text/markdown;charset=utf-8",K=W.cards.map(Xe=>Xe.text.trim()).filter(Boolean).join(`
304
304
 
305
305
  ---
306
306