memory-eternal 0.10.13 → 0.10.14
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 +4 -2
- package/lib/client.js +1 -1
- package/lib/stall.js +11 -3
- package/package.json +1 -1
- package/web/app.js +1 -1
|
@@ -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.
|
|
4
|
+
"version": "0.10.14",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"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.14** | 2026-10-09 | **The "auto-capture anomaly: turn-stop event may be broken" false alarm is fixed at the root.** The scene: during a long turn lasting tens of minutes, `agent/inbox/claimed` fires **repeatedly inside one turn**, while `turn-stopping` only arrives when the turn really ends — so any 20-minute window necessarily shows "≥3 starts, 0 stops", and the check mistook an **in-flight** turn for a renamed event, pushing a red alert at the user (the "39/18" line measured in this session came from exactly that). Fix: one hard precondition — "the event was renamed/removed" is a **process-lifetime** property, not a window property: if this run has seen even one stop, the wiring works and 0 stops in the window merely means the current turn is still running (`evaluateStall` gains `everStopped` and reports `reason: wiring-proven | no-stop-ever`); only "zero stops since this process started **and** continuous starts inside the window" alerts. At the same time the alert is downgraded from `fail` to `warn` — an inference no longer paints capture health red nor masquerades as an auto-capture anomaly (real failures still turn it red on their own). Three checks in `tests/stall.test.mjs` lock the old and new behaviour (long turn stays quiet / a real rename alerts / `everStopped` injectable). `npm test` now runs **305 checks** |
|
|
364
365
|
| **v0.10.13** | 2026-10-09 | **Orphan `web.js` processes are finally reachable (#11 item 1, the POSIX branch)**. The scene (alario-tang, macOS arm64): the watchdog died first and the lock file was gone, leaving a stale web on port 7999 with `PPID=1`; `stop`/`restart` read lock slots and found no target, `watchdog --reap` only matched `watchdog.js`, and the panel button rides the same path — so drift could be reported but never healed, and the user had to `kill` by hand. Two concrete gaps: ① **on POSIX the port occupant only came back as a process name** — `lsof -Fpcn` yields `node` for the `c` field rather than a command line, while the "should we reap it" test (`looksLikeOurWeb`) requires `memory-eternal` or this install web.js absolute path in the command line → always false → the fallback loops in `stopWatchdogs` / `clearResidentForTakeover` **never killed anything** (Windows uses `Get-CimInstance`, which returns the full command line, so this box never showed it); now a follow-up `ps -p <pid> -o args=` fills in the command line, falling back to the old behaviour when unavailable (better to miss than to misfire), with an injectable `platform` for tests; ② **`--reap` now covers our own webs**: a `listWebs` branch reaps "same port + our web + **its keeper is gone** (that slot watchdog is not alive) + outside the protected set (keepPid / current pid / parent / a `--reap` sibling)", while **a web whose keeper is alive is never touched**, and other ports or foreign `web.js` processes are never killed. Adds `tests/watchdog-orphan-web.test.mjs` (5 checks) and makes two watchdog-only cases state `listWebs: []` explicitly (otherwise reap enumerates the real process table). `npm test` now runs **302 checks** |
|
|
365
|
-
| **v0.10.12** | 2026-10-09 | **Two quiet things that either burn money or leave a hole every day.** ① **Default distiller output cap: 2000 → 4000 (the hard ceiling)**: the logs kept showing "hit the maxTokens=2000 cap → retry the same candidate at 4000", and each occurrence **wasted one LLM call** (hit the cap, then re-run the same candidate at 4000). `maxTokens` is only a ceiling and billing follows what is actually generated, so the default now sits at the hard ceiling: those turns finish in one call; `maxTokenLadder` stays for users who **explicitly** lower `captureMaxTokens`. ② **A cross-site write gate on the plugin API**: `/memory-eternal/api/*` is unauthenticated, so any local web page could POST `/config`, `/audit/approve`, `/write`… New `crossOriginError()` — state-changing requests (non-GET/HEAD/OPTIONS) must be **same-origin with the Host of the request itself**: browsers always send `Origin` on cross-site requests (a mismatch returns 403 `CROSS_ORIGIN_BLOCKED`), while same-origin pages (DSH settings/memory page, the standalone page) and LAN access stay unaffected, curl / CLI / MCP calls without `Origin` keep working, and desktop-shell schemes like `app://` are allowed so nothing breaks. Both the intercepted host routes (`/config` POST, `/restart-self`, `/mcp/action`…) and the shared api entry carry the gate. Adds `tests/csrf-origin.test.mjs` (4 checks: decision matrix incl. LAN / no Origin / null / app://, a cross-site POST getting 403 without reaching business logic, both entry points present, and the default cap equal to the hard ceiling with no ladder retry). `npm test` now runs **297 checks** |
|
|
366
366
|
|
|
367
367
|
<details>
|
|
368
|
-
<summary>Older release notes (v0.10.
|
|
368
|
+
<summary>Older release notes (v0.10.12 and earlier · 39 releases, click to expand)</summary>
|
|
369
369
|
|
|
370
370
|
| Version | Date | Highlights |
|
|
371
371
|
|---|---|---|
|
|
372
|
+
| **v0.10.12** | 2026-10-09 | **Two quiet things that either burn money or leave a hole every day.** ① **Default distiller output cap: 2000 → 4000 (the hard ceiling)**: the logs kept showing "hit the maxTokens=2000 cap → retry the same candidate at 4000", and each occurrence **wasted one LLM call** (hit the cap, then re-run the same candidate at 4000). `maxTokens` is only a ceiling and billing follows what is actually generated, so the default now sits at the hard ceiling: those turns finish in one call; `maxTokenLadder` stays for users who **explicitly** lower `captureMaxTokens`. ② **A cross-site write gate on the plugin API**: `/memory-eternal/api/*` is unauthenticated, so any local web page could POST `/config`, `/audit/approve`, `/write`… New `crossOriginError()` — state-changing requests (non-GET/HEAD/OPTIONS) must be **same-origin with the Host of the request itself**: browsers always send `Origin` on cross-site requests (a mismatch returns 403 `CROSS_ORIGIN_BLOCKED`), while same-origin pages (DSH settings/memory page, the standalone page) and LAN access stay unaffected, curl / CLI / MCP calls without `Origin` keep working, and desktop-shell schemes like `app://` are allowed so nothing breaks. Both the intercepted host routes (`/config` POST, `/restart-self`, `/mcp/action`…) and the shared api entry carry the gate. Adds `tests/csrf-origin.test.mjs` (4 checks: decision matrix incl. LAN / no Origin / null / app://, a cross-site POST getting 403 without reaching business logic, both entry points present, and the default cap equal to the hard ceiling with no ladder retry). `npm test` now runs **297 checks** |
|
|
372
373
|
| **v0.10.11** | 2026-10-09 | **One more layer of tolerance in distiller-output parsing: unescaped quotes inside string values (`Expected ',' or '}' after property value`)**. The scene: `UNPARSEABLE_OUTPUT` dominates the `fail` log (103 of the last 600 entries); the freshest one carries the full picture — `JSON.parse 原始报错:Expected ',' or '}' after property value in JSON at position 1913`, with an excerpt whose body reads `运行 "npm test" 时…`: the model wrote a quote into the string as content, so the parser took it for the string terminator. That is a **different** class from the bare control characters fixed in #24 (a control character is illegal inside a string, a quote merely needs escaping). Fix: new `repairUnescapedQuotes()` — a `"` only ends the string when the next non-space character is `:`, `,`, `}`, `]` or end-of-text, otherwise it is escaped as content; it participates **only as the last candidate** and the result must still pass the card health check (usable title + body ≥20 chars) — better to fail than to write a bad card. Guarded by `tests/capture-json-repair.test.mjs` (4 checks: identity on valid JSON, the live unescaped-quote case, control chars + quotes combined, truncation still failing honestly). `npm test` now runs **293 checks** |
|
|
373
374
|
| **v0.10.10** | 2026-10-09 | **The review centre can finally show card bodies, and you can approve/reject in place (issue #27)**. The scene (thinrflbtlm): the review centre listed only "title / kind / source / severity / reason" and **never the body**; and since v0.10.0 pending cards live physically quarantined, while the library lists approved cards only — so "no review without manual confirmation" made auditing pointless. ① Every row gains a ▸/▾ toggle (clicking the title works too) that **expands the frontmatter + body in place**, rendered with the **same Markdown pipeline as the card reader** (`renderMd` + `splitFrontmatter`), not raw text; ② the body is fetched **on demand** (`GET /card?path=…&status=pending|rejected`; the server still only allows paths that really sit in the queue, unknown paths keep returning 403 `CARD_NOT_APPROVED`), so the list endpoint never ships a whole store of bodies; ③ the expanded block carries **✓ approve / ✕ reject** directly (the recycle-bin tab uses ✓ restore / 🗑 delete), while bulk selection and the toolbar stay as they were; ④ approving/rejecting collapses and refreshes automatically, with both language dictionaries updated. Adds `tests/issue-fixes-6.test.mjs` (2 checks: a pending body is readable and leaves the queue once approved, plus source guards for on-demand fetching and the deny code). `npm test` now runs **289 checks** |
|
|
374
375
|
| **v0.10.9** | 2026-10-09 | **Root fix for the upgrade-window port drift that turned the memory page into "connection refused" (issue #29)**. The scene: a detached instance left by the old package sat on 7999 without reporting a version → the host `ensureWebServer()` could not recognise it, drifted to 8000 and wrote that into `web-info`; the plugin's self-heal then reaped the instance on 8000 as a stray → `web-info` dangled on a dead port (the page loaded once, then "connection refused 127.0.0.1" after a refresh). ① **The library page now uses a host same-origin shell**: the host gains `/memory-eternal/ui/app` (same index.html as the config page `/ui/config`, where `<script src="app.js">` resolves to `/ui/app.js`), so the iframe address is **decoupled from whichever port the resident instance listens on** (an older host still falls back to `web-info`). ② **`/web-info` converges live**: before answering, it probes "configured port → recorded port", uses whichever is serving, and reports `alive:false` when neither is (no more advertising a dead address); it is also rewritten right after a successful self-heal restart and whenever the interval keeper sees the instance alive. ③ **No more blind port drift**: new `looksLikeOurWeb()` (lenient detection of "ours but not reporting a version": any `ok:true` from /overview, /web-info or /budget) gives self-heal a 6s takeover window before drifting, and logs it when it does drift. ④ **Backoff retry in the health check**: `inspectResident()` no longer concludes "old instance that does not report its version" from a single missed probe (that verdict triggers a needless replacement on every start), it retries with backoff first. Adds `tests/issue-fixes-5.test.mjs` (4 checks). `npm test` now runs **287 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.14** | 2026-10-09 | **「自动沉淀异常:轮次收尾事件疑似失效」误报根治**。现场:几十分钟的长回合里,`agent/inbox/claimed` 在**一轮对话中途**也会多次触发,而 `turn-stopping` 要等回合真的结束才来 —— 于是 20 分钟窗口必然「开始 ≥3、收尾 0」,判据把**还在进行中**的轮次当成「事件被改名」,直接把红警报推给用户(本会话实测那条「累计 39/18」就是这么来的)。修法:加一条硬前提 —— 「事件被改名/移除」是**进程生命周期**属性,不是窗口属性:本次运行里只要收到过哪怕一次收尾,通路就是好的,窗口内 0 收尾只说明当前轮次还没结束(`evaluateStall` 新增 `everStopped`,并在返回值里给出 `reason: wiring-proven | no-stop-ever`);只有「本次运行至今 **0** 收尾 + 窗口内持续开始」才报警。同时把这条**推断**从 `fail` 降级为 `warn` —— 不再由它把沉淀健康状态翻红、更不冒充「自动沉淀异常」(真故障会由写卡失败自己报红)。`tests/stall.test.mjs` 新增 3 项锁住新旧行为(长回合不报 / 真改名要报 / `everStopped` 可注入)。`npm test` 共 **305 项** |
|
|
396
397
|
| **v0.10.13** | 2026-10-09 | **孤儿 `web.js` 终于收得掉了(#11「待做 1」的 POSIX 分支)**。现场(alario-tang,macOS arm64):watchdog 先死 + 锁文件丢失 → 端口 7999 上留着一个 `PPID=1` 的旧 web;`stop`/`restart` 读锁槽找不到目标、`watchdog --reap` 只认 `watchdog.js`、面板按钮又走同一条链路 → 漂移只能报不能自愈,用户被迫手动 `kill`。查出**两个具体缺口**:① **POSIX 侧端口占用者只拿到进程名** —— `lsof -Fpcn` 的 `c` 字段是 `node` 而不是命令行,而「要不要收」的判据(`looksLikeOurWeb`)要求命令行含 `memory-eternal` 或本机 `web.js` 绝对路径 → 判据恒假 → `stopWatchdogs` / `clearResidentForTakeover` 的兜底循环**一次都不杀**(Windows 走 `Get-CimInstance` 拿完整命令行,所以本机从未暴露);现在补一次 `ps -p <pid> -o args=`,拿不到才退回旧行为(宁可不杀、也不误杀),并让 `platform` 可注入以便单测。② **`--reap` 扩到自家 web**:新增 `listWebs` 分支,把「同端口 + 是自家 web + **托管者已死**(该槽 watchdog 不存活)+ 不在保护集合(keepPid / 当前 pid / 父进程 / 带 `--reap` 的同类)」的 web 一并回收;**托管者还活着的 web 一律不动**,别的端口、别人的 `web.js` 一个不杀。新增 `tests/watchdog-orphan-web.test.mjs`(5 项),并把两条只谈 watchdog 的旧用例显式声明 `listWebs: []`(否则 reap 会去列本机真实进程表)。`npm test` 共 **302 项** |
|
|
397
|
-
| **v0.10.12** | 2026-10-09 | **两件「不显眼但每天都在扣钱 / 留洞」的事**。① **蒸馏输出上限默认 2000 → 4000(= 硬顶)**:日志实测高频出现「输出撞到 maxTokens=2000 上限 → 以 4000 重试同一候选」,每一次都是**白烧一次 LLM 调用**(先撞顶、再拿 4000 重跑同一个候选)。maxTokens 只是上限、按实际生成量计费,所以默认直接放到硬顶:同样的轮次一次调用就够;`maxTokenLadder` 仍保留给**显式**把 `captureMaxTokens` 调小的用户。② **插件 API 加跨站写入闸门**:`/memory-eternal/api/*` 无鉴权,本机任意网页都能 POST `/config`、`/audit/approve`、`/write`… 新增 `crossOriginError()` —— 状态变更请求(非 GET/HEAD/OPTIONS)必须与请求自身的 `Host` **同源**:浏览器跨站请求一定带 `Origin`(与 Host 不同源即 403 `CROSS_ORIGIN_BLOCKED`);同源页面(DSH 设置页/记忆页、独立页)与局域网访问都同源不受影响;没有 Origin 的 curl / CLI / MCP 照常放行;`app://` 这类桌面壳自定义 scheme 也放行以免误伤。宿主自己的拦截路由(`/config` POST、`/restart-self`、`/mcp/action`…)与共享 api 入口**都**装了这道闸门。新增 `tests/csrf-origin.test.mjs`(4 项:判定矩阵含局域网 / 无 Origin / null / app://、跨站 POST 被 403 且不进业务逻辑、两处入口都在、默认上限=硬顶且无 ladder 重试)。`npm test` 共 **297 项** |
|
|
398
398
|
|
|
399
399
|
<details>
|
|
400
|
-
<summary>更早的更新日志(v0.10.
|
|
400
|
+
<summary>更早的更新日志(v0.10.12 及以前 · 共 39 版,点开查看)</summary>
|
|
401
401
|
|
|
402
402
|
| 版本 | 日期 | 关键改动 |
|
|
403
403
|
|---|---|---|
|
|
404
|
+
| **v0.10.12** | 2026-10-09 | **两件「不显眼但每天都在扣钱 / 留洞」的事**。① **蒸馏输出上限默认 2000 → 4000(= 硬顶)**:日志实测高频出现「输出撞到 maxTokens=2000 上限 → 以 4000 重试同一候选」,每一次都是**白烧一次 LLM 调用**(先撞顶、再拿 4000 重跑同一个候选)。maxTokens 只是上限、按实际生成量计费,所以默认直接放到硬顶:同样的轮次一次调用就够;`maxTokenLadder` 仍保留给**显式**把 `captureMaxTokens` 调小的用户。② **插件 API 加跨站写入闸门**:`/memory-eternal/api/*` 无鉴权,本机任意网页都能 POST `/config`、`/audit/approve`、`/write`… 新增 `crossOriginError()` —— 状态变更请求(非 GET/HEAD/OPTIONS)必须与请求自身的 `Host` **同源**:浏览器跨站请求一定带 `Origin`(与 Host 不同源即 403 `CROSS_ORIGIN_BLOCKED`);同源页面(DSH 设置页/记忆页、独立页)与局域网访问都同源不受影响;没有 Origin 的 curl / CLI / MCP 照常放行;`app://` 这类桌面壳自定义 scheme 也放行以免误伤。宿主自己的拦截路由(`/config` POST、`/restart-self`、`/mcp/action`…)与共享 api 入口**都**装了这道闸门。新增 `tests/csrf-origin.test.mjs`(4 项:判定矩阵含局域网 / 无 Origin / null / app://、跨站 POST 被 403 且不进业务逻辑、两处入口都在、默认上限=硬顶且无 ladder 重试)。`npm test` 共 **297 项** |
|
|
404
405
|
| **v0.10.11** | 2026-10-09 | **蒸馏输出解析再补一层容错:字符串里的裸引号(`Expected ',' or '}' after property value`)**。现场:`fail` 日志里 `UNPARSEABLE_OUTPUT` 占大头(最近 600 条里 103 条),最近一条带全了现场 —— `JSON.parse 原始报错:Expected ',' or '}' after property value in JSON at position 1913`,摘录里能看到正文写着 `运行 "npm test" 时…`:模型把引号当正文直接写进字符串,于是它被当成字符串结束符。这与 #24 修过的「裸控制字符」是**不同**的一类(控制字符在字符串内非法,引号只是需要转义)。修法:新增 `repairUnescapedQuotes()` —— 一个 `"` 后面(跳过空白)紧跟 `:` `,` `}` `]` 或文本结尾才算字符串结束,否则当正文补 `\"`;它**只作为最后一个候选**参与解析,且必须仍通过卡片体检(标题合格 + 正文 ≥20 字)才被接受 —— 宁可不修,也不写坏卡。回归守卫 `tests/capture-json-repair.test.mjs`(4 项:合法 JSON 恒等变换、裸引号现场、控制字符与裸引号叠加、截断仍如实失败)。`npm test` 共 **293 项** |
|
|
405
406
|
| **v0.10.10** | 2026-10-09 | **审核中心能看正文了 + 就地批准/驳回(issue #27)**。现场(thinrflbtlm):审核中心过去只列「标题 / 类型 / 来源 / 严重度 / 原因」,**看不到正文**;而 v0.10.0 之后待审卡物理隔离在 `quarantine`、知识卡界面只列已审核卡 —— 于是「不人工确认就没法审」,审核形同虚设。① 每行新增 ▸/▾(点标题也行)**就地展开 frontmatter + 正文**,用与卡片详情**同一套 Markdown 渲染**(`renderMd` + `splitFrontmatter`),不是裸文本;② 正文**按需拉取**(`GET /card?path=…&status=pending|rejected`,服务端只放行确实在审核队列里的 path,未知 path 仍然 403 `CARD_NOT_APPROVED`),列表接口不塞整库正文;③ 展开块里直接给 **✓ 批准 / ✕ 驳回**(回收站页签是 ✓ 恢复 / 🗑 删除),批量勾选与外层工具条保持原样;④ 批/驳后自动收起并刷新,中英双语词条齐全。新增 `tests/issue-fixes-6.test.mjs`(2 项:待审正文读得到且批准后离开队列、源码守卫含「按需拉取」与「越权拒绝码」)。`npm test` 共 **289 项** |
|
|
406
407
|
| **v0.10.9** | 2026-10-09 | **升级期端口漂移把记忆页打成「拒绝连接」的根治(issue #29)**。现场:旧包遗留的 detached 实例占着 7999 且不自报版本 → 宿主 `ensureWebServer()` 认不出它、向后漂到 8000 并写进 `web-info`;而插件的自愈随后把 8000 上的实例当野实例收掉 → `web-info` 永久悬空,客户端 iframe 指向死端口(首次能进、刷新即「拒绝连接 127.0.0.1」)。① **记忆库页改走 host 同源壳**:宿主新增 `/memory-eternal/ui/app`(与配置页 `/ui/config` 同一份 index.html,其中 `<script src="app.js">` 相对路径正好解析到 `/ui/app.js`),iframe 地址从此与「常驻实例监听哪个端口」**彻底解耦**(旧宿主 404 时仍回落 `web-info`)。② **`/web-info` 实时收敛**:每次读之前先探「配置端口 → 记录端口」,谁在服务就用谁,都不通就如实 `alive:false`(不再把死地址当活的);自愈替换成功后与 interval 保活探到活时都就地回写。③ **不再盲目漂端口**:新增 `looksLikeOurWeb()`(宽松识别「自家但不自报版本」的占用者:/overview、/web-info、/budget 任一 ok:true 即认),疑似自家时先给自愈留 6s 接管窗口,等不到才漂并留日志。④ **体检退避重试**:`inspectResident()` 一次探针打空不再直接判「旧版不自报版本」(那个结论会触发误替换,每次启动都白起一次实例),退避重试后才下结论。新增 `tests/issue-fixes-5.test.mjs`(4 项:宽松识别真/假阳性、等自愈接管不漂端口、宿主与客户端源码守卫)。`npm test` 共 **287 项** |
|
package/index.js
CHANGED
|
@@ -972,10 +972,12 @@ export function apply(ctx, config) {
|
|
|
972
972
|
// 沉淀停滞探测:只有「有轮次在跑」且「管线 15 分钟一个活口都没有」才报(真死才报)。
|
|
973
973
|
// 计数器对不上是常态(子代理轮次、被取消的轮次都不发 turn-stopping),单看计数会误报刷屏。
|
|
974
974
|
const stallTimer = setInterval(() => {
|
|
975
|
-
const { alert, started, stopped, windowMs } = evaluateStall({ claimedAt, stoppedAt })
|
|
975
|
+
const { alert, started, stopped, windowMs } = evaluateStall({ claimedAt, stoppedAt, everStopped: turnsStopped })
|
|
976
976
|
if (alert && !stallAlerted) {
|
|
977
977
|
stallAlerted = true
|
|
978
|
-
|
|
978
|
+
// 用 warn 不用 fail:这是「本次运行至今一次收尾都没收到」的**推断**,不该由它把沉淀健康状态
|
|
979
|
+
// 翻红、更不该冒充「自动沉淀异常」去打扰用户(真故障会由写卡失败自己报红)。
|
|
980
|
+
logCapture('system', 'warn', `本次运行至今没收到过任何一次「轮次收尾」事件(窗口内开始 ${started} 个、累计 ${turnsStarted} 个)——DSH 可能改了事件名或作用域,沉淀不会自动触发`)
|
|
979
981
|
}
|
|
980
982
|
}, 5 * 60 * 1000)
|
|
981
983
|
ctx.effect(() => () => clearInterval(stallTimer), 'memory-eternal: capture stall watch')
|
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 Sa(t){t.effect(()=>t.locale.register(lt,{zh:Xt,en:Jt}),"memory-eternal: locale");let o=t.locale.bind(lt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:lt,order:25,label:()=>o("nav"),locale:lt,inject:()=>({})},()=>n.default.createElement(Na,{t:o}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${lt}:footer`,order:100,label:()=>o("nav"),locale:lt,inject:()=>({})},r=>n.default.createElement(Ma,{t:o,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Ca(){let[t,o]=(0,n.useState)(_+"/ui/app");return(0,n.useEffect)(()=>{let r=!0,m=()=>fetch("/memory-eternal/api/web-info").then(d=>d.json()).then(d=>{r&&d&&d.ok&&d.alive!==!1&&d.url&&o(d.url.endsWith("/")?d.url:d.url+"/")}).catch(()=>{});return fetch(_+"/ui/app",{method:"HEAD"}).then(d=>{if(r&&!d.ok)return m()}).catch(()=>r?m():void 0),()=>{r=!1}},[]),t}function Na({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Fe}),(0,e.jsx)(ta,{t,onReload:()=>{},version:0})]})}function Ma({t,locale:o,wide:r}){let[m,d]=(0,n.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Fe}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>d(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Qt,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),m&&(0,e.jsx)(Da,{t,locale:o,onClose:()=>d(!1)})]})}function Da({t,locale:o,onClose:r,tab:m}){let[d,i]=(0,n.useState)(!1),w=(0,n.useRef)(null),C=Ca(),x=(0,n.useCallback)(I=>o&&typeof o.subscribe=="function"?o.subscribe(I):()=>{},[o]),A=(0,n.useCallback)(()=>o&&typeof o.getSnapshot=="function"?o.getSnapshot():null,[o]),k=(0,n.useSyncExternalStore)(x,A),h=k&&(k.active==="zh"||k.active==="en")?k.active:null,u=(0,n.useRef)(null);u.current===null&&h&&(u.current=h);let D=(0,n.useMemo)(()=>{let I=u.current?`lang=${u.current}`:"";if(m==="config")return`${_}/ui/config?tab=config${I?`&${I}`:""}`;let v=C.replace(/\/$/,""),T=v.endsWith("/ui/app")?"?":"/?";return m?`${v}${T}tab=${m}${I?`&${I}`:""}`:I?`${v}${T}${I}`:C},[C,m]);return(0,n.useEffect)(()=>{let I=w.current&&w.current.contentWindow;I&&h&&I.postMessage({source:"memory-eternal",type:"locale",lang:h},"*")},[h]),(0,n.useEffect)(()=>{let I=v=>{v.key==="Escape"&&r()};return window.addEventListener("keydown",I),()=>window.removeEventListener("keydown",I)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Fe}),(0,e.jsx)("div",{className:"me-modal"+(d?" me-modal-full":""),onClick:I=>I.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:()=>i(I=>!I),"aria-label":t(d?"exitFullscreen":"enterFullscreen"),title:t(d?"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:d?"\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:w,src:D,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 Qt(){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 bt="0.10.
|
|
301
|
+
`;function Sa(t){t.effect(()=>t.locale.register(lt,{zh:Xt,en:Jt}),"memory-eternal: locale");let o=t.locale.bind(lt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:lt,order:25,label:()=>o("nav"),locale:lt,inject:()=>({})},()=>n.default.createElement(Na,{t:o}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${lt}:footer`,order:100,label:()=>o("nav"),locale:lt,inject:()=>({})},r=>n.default.createElement(Ma,{t:o,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Ca(){let[t,o]=(0,n.useState)(_+"/ui/app");return(0,n.useEffect)(()=>{let r=!0,m=()=>fetch("/memory-eternal/api/web-info").then(d=>d.json()).then(d=>{r&&d&&d.ok&&d.alive!==!1&&d.url&&o(d.url.endsWith("/")?d.url:d.url+"/")}).catch(()=>{});return fetch(_+"/ui/app",{method:"HEAD"}).then(d=>{if(r&&!d.ok)return m()}).catch(()=>r?m():void 0),()=>{r=!1}},[]),t}function Na({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Fe}),(0,e.jsx)(ta,{t,onReload:()=>{},version:0})]})}function Ma({t,locale:o,wide:r}){let[m,d]=(0,n.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Fe}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>d(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Qt,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),m&&(0,e.jsx)(Da,{t,locale:o,onClose:()=>d(!1)})]})}function Da({t,locale:o,onClose:r,tab:m}){let[d,i]=(0,n.useState)(!1),w=(0,n.useRef)(null),C=Ca(),x=(0,n.useCallback)(I=>o&&typeof o.subscribe=="function"?o.subscribe(I):()=>{},[o]),A=(0,n.useCallback)(()=>o&&typeof o.getSnapshot=="function"?o.getSnapshot():null,[o]),k=(0,n.useSyncExternalStore)(x,A),h=k&&(k.active==="zh"||k.active==="en")?k.active:null,u=(0,n.useRef)(null);u.current===null&&h&&(u.current=h);let D=(0,n.useMemo)(()=>{let I=u.current?`lang=${u.current}`:"";if(m==="config")return`${_}/ui/config?tab=config${I?`&${I}`:""}`;let v=C.replace(/\/$/,""),T=v.endsWith("/ui/app")?"?":"/?";return m?`${v}${T}tab=${m}${I?`&${I}`:""}`:I?`${v}${T}${I}`:C},[C,m]);return(0,n.useEffect)(()=>{let I=w.current&&w.current.contentWindow;I&&h&&I.postMessage({source:"memory-eternal",type:"locale",lang:h},"*")},[h]),(0,n.useEffect)(()=>{let I=v=>{v.key==="Escape"&&r()};return window.addEventListener("keydown",I),()=>window.removeEventListener("keydown",I)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Fe}),(0,e.jsx)("div",{className:"me-modal"+(d?" me-modal-full":""),onClick:I=>I.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:()=>i(I=>!I),"aria-label":t(d?"exitFullscreen":"enterFullscreen"),title:t(d?"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:d?"\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:w,src:D,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 Qt(){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 bt="0.10.14";function za({t,inModal:o,onClose:r,onFull:m,full:d}){let[i,w]=(0,n.useState)(null),[C,x]=(0,n.useState)([]),[A,k]=(0,n.useState)(!0),[h,u]=(0,n.useState)(null),[D,I]=(0,n.useState)(""),[v,T]=(0,n.useState)("all"),[N,j]=(0,n.useState)(""),[Q,xe]=(0,n.useState)("all"),[q,Re]=(0,n.useState)("all"),[U,me]=(0,n.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),P={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&P[b])return P[b]}catch{}return"cards"}),[le,F]=(0,n.useState)(()=>new Set([U])),y=(0,n.useCallback)(b=>{F(P=>P.has(b)?P:new Set(P).add(b)),me(b)},[]),[H,M]=(0,n.useState)("recent"),[ne,K]=(0,n.useState)(null),X=(0,n.useRef)(null),re=(0,n.useRef)(null),[f,W]=(0,n.useState)(!1),[de,Y]=(0,n.useState)(null),[ie,we]=(0,n.useState)(!1),[Ae,Me]=(0,n.useState)(!1),[ve,Ce]=(0,n.useState)(!0),[et,Ze]=(0,n.useState)(null),[Ve,nt]=(0,n.useState)(0),tt=(0,n.useCallback)(()=>nt(b=>b+1),[]),Te=i?.quarantine?.pending??0,Be=i?.quarantine?.deleted??0,pe=(0,n.useRef)(null),[Ye,Ne]=(0,n.useState)(0),[c,s]=(0,n.useState)(!1),E=(0,n.useRef)(null),se=(0,n.useRef)(null),$=(0,n.useCallback)(async(b=v,P=N,Z=q,ke={})=>{let G=ke.append===!0;try{G?s(!0):(k(!0),I(""));let he=new URLSearchParams;b&&b!=="all"&&he.set("kind",b),P.trim()&&he.set("q",P.trim()),he.set("status",Z||"all"),he.set("limit",String(va)),he.set("offset",String(ke.offset||0)),H==="title"&&he.set("sort","title");let Ie=await(await fetch(`${_}/cards?${he.toString()}`)).json();if(Ie.ok){let Ue=Ie.cards||[];x(He=>G?[...He,...Ue.filter(Ke=>!He.some(vt=>vt.path===Ke.path))]:Ue),Ne(Number(Ie.total)||Ue.length)}}catch(he){I(String(he&&he.message?he.message:he))}finally{G?s(!1):k(!1)}},[v,N,q,H]),ge=(0,n.useCallback)(async()=>{try{let[b,P]=await Promise.all([fetch(`${_}/overview`).then(Z=>Z.json()),fetch(`${_}/capture-log`).then(Z=>Z.json()).catch(()=>null)]);b.ok&&w(b),u(P&&P.ok&&P.available&&P.health||null)}catch(b){I(String(b&&b.message?b.message:b))}finally{k(!1)}},[]),a=async b=>{try{let Z=await(await fetch(`${_}/delete?path=${encodeURIComponent(b)}`)).json();Z.ok?(K({ok:!0,msg:t("deleted")}),await ge(),tt()):K({ok:!1,msg:Z.error||t("deleteFail")})}catch{K({ok:!1,msg:t("deleteFail")})}X.current&&clearTimeout(X.current),X.current=setTimeout(()=>K(null),2e3)};(0,n.useEffect)(()=>(ge(),()=>{pe.current&&clearTimeout(pe.current)}),[ge]);let l=b=>{j(b),pe.current&&clearTimeout(pe.current),pe.current=setTimeout(()=>$(v,b,q),280)},L=b=>{T(b),$(b,N,q)};(0,n.useEffect)(()=>{U==="cards"&&$(v,N,q)},[q,U,Ve,H]),(0,n.useEffect)(()=>{if(U!=="cards"||A||c||C.length>=Ye)return;let b=E.current,P=se.current;if(!b)return;let Z=new IntersectionObserver(ke=>{ke[0]&&ke[0].isIntersecting&&$(v,N,q,{append:!0,offset:C.length})},{root:P||null,rootMargin:"400px"});return Z.observe(b),()=>Z.disconnect()},[U,C,Ye,A,c,v,N,q,$]);let B=async b=>{try{let P=b.status;if(!P&&b.path)try{let ze=((await fetch(`${_}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(Ie=>Ie.json())).cards||[]).find(Ie=>Ie.path===b.path);ze&&(P=ze.status)}catch{}let Z=P&&P!=="approved"?`&status=${encodeURIComponent(P)}`:"",ke=await fetch(`${_}/card?path=${encodeURIComponent(b.path||b.id)}${Z}`),G=await ke.json();G.ok?Y({path:G.path,title:b.title,text:G.text}):I(G.error||`HTTP ${ke.status}`)}catch(P){I(String(P&&P.message?P.message:P))}},J=async(b,P)=>{W(!0);try{let Z=await fetch(`${_}/export`);if(!Z.ok)throw new Error("HTTP "+Z.status);let ke=await Z.json();if(!ke.ok)throw new Error(ke.error||"export failed");let G,he,ze;if(b==="json"){he="memory-vault.json",ze="application/json";let He=ke.cards.map(Ke=>({path:Ke.path,title:Ke.title,kind:Ke.kind,status:Ke.status,store:Ke.store,text:Ke.text}));G=JSON.stringify({format:ke.format||"memory-eternal-vault",formatVersion:ke.formatVersion||1,exportedAt:ke.exportedAt||new Date().toISOString(),count:He.length,cards:He},null,2)}else he="memory-vault.md",ze="text/markdown;charset=utf-8",G=ke.cards.map(He=>He.text.trim()).filter(Boolean).join(`
|
|
302
302
|
|
|
303
303
|
---
|
|
304
304
|
|
package/lib/stall.js
CHANGED
|
@@ -17,13 +17,21 @@
|
|
|
17
17
|
* @param {number} [input.now] 当前时间
|
|
18
18
|
* @param {number} [input.windowMs] 观察窗口,默认 20 分钟
|
|
19
19
|
* @param {number} [input.minStarted] 窗口内至少开始几个轮次才判停滞,默认 3
|
|
20
|
-
* @
|
|
20
|
+
* @param {number} [input.everStopped] 本次运行**至今**收到的收尾事件数(缺省 = stoppedAt.length)
|
|
21
|
+
* @returns {{alert:boolean, started:number, stopped:number, windowMs:number, reason:string}}
|
|
21
22
|
*/
|
|
22
|
-
export function evaluateStall({ claimedAt = [], stoppedAt = [], now = Date.now(), windowMs = 20 * 60 * 1000, minStarted = 3 } = {}) {
|
|
23
|
+
export function evaluateStall({ claimedAt = [], stoppedAt = [], now = Date.now(), windowMs = 20 * 60 * 1000, minStarted = 3, everStopped } = {}) {
|
|
23
24
|
const from = now - windowMs
|
|
24
25
|
const started = claimedAt.filter((t) => t >= from).length
|
|
25
26
|
const stopped = stoppedAt.filter((t) => t >= from).length
|
|
26
|
-
|
|
27
|
+
// 2026-10-09:再加一条硬前提 —— 「事件被改名/移除」是**进程生命周期**属性,不是窗口属性。
|
|
28
|
+
// 只要本次运行里见过哪怕一次收尾,事件通路就是通的;窗口内 0 收尾只说明「当前这一轮还没结束」
|
|
29
|
+
// (实测:一轮几十分钟的对话里 agent/inbox/claimed 会多次触发,而 turn-stopping 要等回合真的
|
|
30
|
+
// 结束才来 —— 20 分钟窗口必然 3 次 claim / 0 收尾,于是长回合被误报成「收尾事件疑似失效」,
|
|
31
|
+
// 并且把「自动沉淀异常」这个红警报推给了用户)。真的被改名时,本次运行会是 0 收尾。
|
|
32
|
+
const ever = Number.isFinite(everStopped) ? Number(everStopped) : stoppedAt.length
|
|
33
|
+
if (ever > 0) return { alert: false, started, stopped, windowMs, reason: 'wiring-proven' }
|
|
34
|
+
return { alert: started >= minStarted && stopped === 0, started, stopped, windowMs, reason: 'no-stop-ever' }
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/** 时间戳数组的裁剪上限:只保留窗口相关的近邻,避免长跑无界增长。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.14",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
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 Vg(){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.
|
|
303
|
+
`;function Vg(){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.14";function ef({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,v.useState)(null),[s,u]=(0,v.useState)([]),[f,y]=(0,v.useState)(!0),[h,m]=(0,v.useState)(null),[k,M]=(0,v.useState)(""),[w,I]=(0,v.useState)("all"),[d,c]=(0,v.useState)(""),[g,N]=(0,v.useState)("all"),[P,j]=(0,v.useState)("all"),[D,O]=(0,v.useState)(()=>{try{let L=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),V={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(L&&V[L])return V[L]}catch{}return"cards"}),[Y,_]=(0,v.useState)(()=>new Set([D])),T=(0,v.useCallback)(L=>{_(V=>V.has(L)?V:new Set(V).add(L)),O(L)},[]),[K,A]=(0,v.useState)("recent"),[ce,Z]=(0,v.useState)(null),re=(0,v.useRef)(null),me=(0,v.useRef)(null),[z,G]=(0,v.useState)(!1),[he,te]=(0,v.useState)(null),[pe,Fe]=(0,v.useState)(!1),[ht,Qe]=(0,v.useState)(!1),[_e,He]=(0,v.useState)(!0),[bn,un]=(0,v.useState)(null),[Gt,Kn]=(0,v.useState)(0),Sn=(0,v.useCallback)(()=>Kn(L=>L+1),[]),ot=l?.quarantine?.pending??0,yt=l?.quarantine?.deleted??0,xe=(0,v.useRef)(null),[qt,We]=(0,v.useState)(0),[S,x]=(0,v.useState)(!1),Q=(0,v.useRef)(null),ge=(0,v.useRef)(null),q=(0,v.useCallback)(async(L=w,V=d,le=P,Ae={})=>{let ne=Ae.append===!0;try{ne?x(!0):(y(!0),M(""));let Le=new URLSearchParams;L&&L!=="all"&&Le.set("kind",L),V.trim()&&Le.set("q",V.trim()),Le.set("status",le||"all"),Le.set("limit",String(Wg)),Le.set("offset",String(Ae.offset||0)),K==="title"&&Le.set("sort","title");let lt=await(await fetch(`${J}/cards?${Le.toString()}`)).json();if(lt.ok){let jt=lt.cards||[];u(Et=>ne?[...Et,...jt.filter(Bt=>!Et.some(Xo=>Xo.path===Bt.path))]:jt),We(Number(lt.total)||jt.length)}}catch(Le){M(String(Le&&Le.message?Le.message:Le))}finally{ne?x(!1):y(!1)}},[w,d,P,K]),ke=(0,v.useCallback)(async()=>{try{let[L,V]=await Promise.all([fetch(`${J}/overview`).then(le=>le.json()),fetch(`${J}/capture-log`).then(le=>le.json()).catch(()=>null)]);L.ok&&a(L),m(V&&V.ok&&V.available&&V.health||null)}catch(L){M(String(L&&L.message?L.message:L))}finally{y(!1)}},[]),p=async L=>{try{let le=await(await fetch(`${J}/delete?path=${encodeURIComponent(L)}`)).json();le.ok?(Z({ok:!0,msg:e("deleted")}),await ke(),Sn()):Z({ok:!1,msg:le.error||e("deleteFail")})}catch{Z({ok:!1,msg:e("deleteFail")})}re.current&&clearTimeout(re.current),re.current=setTimeout(()=>Z(null),2e3)};(0,v.useEffect)(()=>(ke(),()=>{xe.current&&clearTimeout(xe.current)}),[ke]);let b=L=>{c(L),xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>q(w,L,P),280)},W=L=>{I(L),q(L,d,P)};(0,v.useEffect)(()=>{D==="cards"&&q(w,d,P)},[P,D,Gt,K]),(0,v.useEffect)(()=>{if(D!=="cards"||f||S||s.length>=qt)return;let L=Q.current,V=ge.current;if(!L)return;let le=new IntersectionObserver(Ae=>{Ae[0]&&Ae[0].isIntersecting&&q(w,d,P,{append:!0,offset:s.length})},{root:V||null,rootMargin:"400px"});return le.observe(L),()=>le.disconnect()},[D,s,qt,f,S,w,d,P,q]);let U=async L=>{try{let V=L.status;if(!V&&L.path)try{let Ze=((await fetch(`${J}/cards?status=all&q=${encodeURIComponent(L.title||"")}&limit=50`).then(lt=>lt.json())).cards||[]).find(lt=>lt.path===L.path);Ze&&(V=Ze.status)}catch{}let le=V&&V!=="approved"?`&status=${encodeURIComponent(V)}`:"",Ae=await fetch(`${J}/card?path=${encodeURIComponent(L.path||L.id)}${le}`),ne=await Ae.json();ne.ok?te({path:ne.path,title:L.title,text:ne.text}):M(ne.error||`HTTP ${Ae.status}`)}catch(V){M(String(V&&V.message?V.message:V))}},oe=async(L,V)=>{G(!0);try{let le=await fetch(`${J}/export`);if(!le.ok)throw new Error("HTTP "+le.status);let Ae=await le.json();if(!Ae.ok)throw new Error(Ae.error||"export failed");let ne,Le,Ze;if(L==="json"){Le="memory-vault.json",Ze="application/json";let Et=Ae.cards.map(Bt=>({path:Bt.path,title:Bt.title,kind:Bt.kind,status:Bt.status,store:Bt.store,text:Bt.text}));ne=JSON.stringify({format:Ae.format||"memory-eternal-vault",formatVersion:Ae.formatVersion||1,exportedAt:Ae.exportedAt||new Date().toISOString(),count:Et.length,cards:Et},null,2)}else Le="memory-vault.md",Ze="text/markdown;charset=utf-8",ne=Ae.cards.map(Et=>Et.text.trim()).filter(Boolean).join(`
|
|
304
304
|
|
|
305
305
|
---
|
|
306
306
|
|