memory-eternal 0.10.25 → 0.10.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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/lib/api.js +21 -3
- package/lib/client.js +1 -1
- package/lib/db.js +32 -0
- 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.26",
|
|
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.26** | 2026-10-13 | **Lock contention now retries asynchronously at the application level instead of using `busy_timeout` (the measured remedy for issues #31/#33)**. Local controlled experiment: while an external process held a write transaction we immediately got `database is locked`, and it **stayed locked even after that process was killed** — the old code had no waiting at all, so a single collision failed permanently (on restricted or externally-occupied drives like A: this shows up as "the whole memory database is broken"). The review rejected `PRAGMA busy_timeout` (`DatabaseSync` from `node:sqlite` is a **synchronous API**, so busy-waiting blocks the host event loop and freezes all of DSH), so we added `withDbRetry()` instead: on a lock, **yield the event loop, sleep briefly and retry** (linear backoff, up to 3 attempts), only on **write routes**; once headers have been sent retrying is explicitly forbidden (`e.noRetry`, preventing double writes), and non-lock errors (data/syntax) are tried once and rethrown unchanged. Tests cover: retry until success / one attempt for non-busy errors / `noRetry` honoured / exhaustion / a source guard against sneaking `busy_timeout` back in. `npm test` now runs **335 checks** |
|
|
364
365
|
| **v0.10.25** | 2026-10-13 | **"Cannot be determined" no longer masquerades as "unmanaged", plus a unified UX for an unwritable DB (closing layer ③ of issue #33)**. ① **Managed state becomes three-valued**: `/version-check` now also reports `resident.managedKnown`, `lock:{path,writable,error}` and `pid` — in restricted environments (sandbox write bans, ACLs, non-system drives) where the watchdog lock file cannot be written, the panel no longer says "unmanaged" (which made users think the instance was broken and restart it repeatedly) but "**management state unknown**: the lock file cannot be written, so it cannot be determined; keep-alive usually still works", together with the real cause and path; `writeWatchdogLock` failures are no longer fully silent either (one traceable line per process). ② **Unified UX when the DB is unwritable**: write routes (delete / merge / card / refine-apply / optimize-execute / …) now **fail fast at the entrance** (503 + `DB_UNAVAILABLE` + recovery guidance), and any write path not enumerated is caught by the outermost wrapper translating raw SQLite text into the same reply; the panel shows a **cross-tab banner** at the top: "the memory database is currently not writable — card reads/writes will fail, but **settings can still be saved**". ③ `dsh-memory doctor` now reports watchdog lock writability (visible at a glance in restricted environments). `npm test` now runs **334 checks** |
|
|
365
|
-
| **v0.10.24** | 2026-10-13 | **"Same goal, different approach" now clusters too (third and final slice of issue #36)**. The two previous versions covered "merge duplicates" and "judge conflicts", but the last piece — one purpose, several workflows/codebases — is **undetectable by body similarity** ("compress with 7z", "distribute as tar.gz", "slim the npm package" read very differently yet share a goal). Approach: **the model first writes one line per card stating what problem it solves, then we cluster by goal** — goals are cached in `refine-goals.json` (each card extracted once; a second scan makes zero calls) and clustered with union-find; hits are grouped as "one goal, N cards" with two actions: **keep all** (records pairwise rulings so it stops asking) or **merge this group** (via the existing /merge; the new card enters the audit queue). *Read-only throughout — the only write is the goal cache.* Entry points: the panel (organize → 🎯 same goal, different approach) and APIs `/refine-goals`, `/refine-goals-keep`; CLI `dsh-memory refine --goals --keep --path A --path B` (the clustering itself needs a model, run it in the panel). `npm test` now runs **332 checks** |
|
|
366
366
|
|
|
367
367
|
<details>
|
|
368
|
-
<summary>Older release notes (v0.10.
|
|
368
|
+
<summary>Older release notes (v0.10.24 and earlier · 51 releases, click to expand)</summary>
|
|
369
369
|
|
|
370
370
|
| Version | Date | Highlights |
|
|
371
371
|
|---|---|---|
|
|
372
|
+
| **v0.10.24** | 2026-10-13 | **"Same goal, different approach" now clusters too (third and final slice of issue #36)**. The two previous versions covered "merge duplicates" and "judge conflicts", but the last piece — one purpose, several workflows/codebases — is **undetectable by body similarity** ("compress with 7z", "distribute as tar.gz", "slim the npm package" read very differently yet share a goal). Approach: **the model first writes one line per card stating what problem it solves, then we cluster by goal** — goals are cached in `refine-goals.json` (each card extracted once; a second scan makes zero calls) and clustered with union-find; hits are grouped as "one goal, N cards" with two actions: **keep all** (records pairwise rulings so it stops asking) or **merge this group** (via the existing /merge; the new card enters the audit queue). *Read-only throughout — the only write is the goal cache.* Entry points: the panel (organize → 🎯 same goal, different approach) and APIs `/refine-goals`, `/refine-goals-keep`; CLI `dsh-memory refine --goals --keep --path A --path B` (the clustering itself needs a model, run it in the panel). `npm test` now runs **332 checks** |
|
|
372
373
|
| **v0.10.23** | 2026-10-13 | **Existing "injected junk cards" can be cleaned up in one click (second slice of issue #36)**. The previous version stopped such cards from being **created** (skill catalogs / `<system-reminder>` arriving as user messages were captured as knowledge after their tags were stripped), but the **backlog remained**: a scan of a real vault immediately found **32** cards unrelated to any conversation (catalog lines like `- \`code-review\`: Review the changes…`, fragments like `用户system-reminder-you-are-teammate-c…`). Adds `findInjectedCards()` (a **proposal only**, it changes nothing; criteria: injection tags present / catalog lines ≥30% of the body / under 200 characters left after stripping / a fragmentary title) and `purgeInjected()` (**goes to the recycle bin, recoverable**, reporting failures per card). Entry points: the panel (organize → 🧹 injected junk cards: scan, recycle one, recycle all) and the CLI `dsh-memory refine --junk [--purge --path P] [--json]`; API `/refine-junk`, `/refine-junk-purge`. `npm test` now runs **331 checks** |
|
|
373
374
|
| **v0.10.22** | 2026-10-13 | **"Conflict scan": similar cards may be merged, conflicts must be judged by a human (first slice of issue #36)**. Until now `/optimize` merged **blindly** on similarity (≥0.55) — yet two semantically close cards with mutually exclusive conclusions score just as high (0.93 measured), so a blind merge quietly "reconciles" the conflict, and `mergeCards` **permanently deletes** the sources. New `lib/refine.js` plus a "organize → 🧭 Conflict scan" panel section: ① **wide recall** (≥0.30, upper bound lifted to 1.0, deliberately including the near-duplicate band — the most dangerous one for blind merging); ② the model only classifies each pair as "duplicate / complement / conflict / unrelated" and states the conflict in one line plus who to keep; ③ **the human decides**: keep A / keep B / keep both / merge; the **scan never writes**, and `keepA|keepB` only moves the yielding card to the **recycle bin** (recoverable); ④ rulings are recorded in `refine-decisions.json` so the same pair is not raised again; ⑤ **safety interlock**: pairs ruled conflicting are **excluded from one-click optimize** entirely. Also fixes one source of redundancy: skill-catalog injections (`- \`skill-name\`: …`) are no longer captured as junk cards (real examples already existed in the vault). API: `/refine`, `/refine-scan`, `/refine-apply`; CLI: `dsh-memory refine` (lists candidates without calling a model; `--apply` records a ruling). `npm test` now runs **329 checks** |
|
|
374
375
|
| **v0.10.21** | 2026-10-13 | **"DB unavailable" goes from silent to visible (issue #33, scoped by review)**. ① **Health check at open**: the old code ran `PRAGMA journal_mode=WAL` and assumed success (that pragma does not throw on failure — it just returns the actual mode), so a read-only file or unwritable directory let the plugin run on silently; now the real journal mode is **read back** and a **write probe** runs (create then drop a table, in the main DB rather than temp), the verdict lands in `dbStatus()`, and an unwritable DB immediately logs the reason plus where to look. ② **`/overview` degrades instead of 500**: when the DB or directory is unavailable it no longer returns 500 (probes, the panel and scripts read 500 as "this port is broken / not our service", which historically started the restart storm and port drift); it now returns 200 with `degraded:true`, `dbError` and `db:{journalMode,writable,error}` while keeping the `vaultDir` / `version` identity fields — so "this is us, but the DB is broken" can finally be expressed separately. ③ **New `dsh-memory doctor`**: DB health self-check (real journal mode / write probe / backup list / recovery guidance) with `--json` for scripting; when the DB is unwritable or suspected corrupt it prints **guidance that never touches data** (stop writers → copy the scene aside → list backups → swap → re-verify). **Deliberately not done** (review conclusion, now guarded by tests so nobody adds it back casually): `busy_timeout` (`DatabaseSync` is a synchronous API, so waiting on a lock blocks the host event loop) and automatic WAL→DELETE downgrade (it **permanently** changes the DB mode, and a rollback journal has worse concurrency, which could worsen lock contention). `npm test` now runs **322 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.26** | 2026-10-13 | **撞锁改用「应用层异步重试」,而不是 `busy_timeout`(issue #31/#33 实测对症)**。本机对照实验:外部进程持写事务时我方立刻拿到 `database is locked`,而且**对方被强杀之后仍然锁着** —— 老实现没有忙等,一撞锁就永久失败(在 A: 盘那类受限 / 被外部占用的现场,表现出来就是「整个记忆库坏了」)。评审已否决 `PRAGMA busy_timeout`(`node:sqlite` 的 `DatabaseSync` 是**同步 API**,忙等会阻塞宿主事件循环、把整个 DSH 卡住),所以改成新增 `withDbRetry()`:撞锁时**让出事件循环睡一小会儿再试**(线性退避,最多 3 次),只用**写路由**;「响应已发出」时显式禁止重试(`e.noRetry`,避免重复写),非锁类错误(数据 / 语法)只试一次并原样抛。测试覆盖:重试到成功 / 非忙错误只试一次 / `noRetry` 生效 / 试满仍失败 / 源码守卫(不许偷偷加 `busy_timeout`)。`npm test` 共 **335 项** |
|
|
396
397
|
| **v0.10.25** | 2026-10-13 | **「判不出来」不再冒充「未托管」+ 库不可写时的统一 UX(issue #33 第③层收口)**。① **托管状态变三态**:`/version-check` 增报 `resident.managedKnown`、`lock:{path,writable,error}` 与 `pid` —— 受限环境(沙箱禁写 / ACL / 非系统盘)写不进看门狗锁文件时,面板不再显示「未托管」(那会让用户以为实例坏了、反复重启),改为「**托管状态未知**:锁文件写不进去 → 无法判断;保活通常仍然有效」,并同时显示真实原因与路径;`writeWatchdogLock` 的失败也不再全静默(每个进程留一条可查的痕)。② **库不可写时统一 UX**:写路由(delete / merge / card / refine-apply / optimize-execute / …)在入口**短路径失败**(503 + `DB_UNAVAILABLE` + 恢复指引),没枚举到的写路径由最外层兜底把 SQLite 原文翻译成同一套答复;面板**跨标签顶部**明示「记忆库当前不可写 —— 卡片读写会失败,但**配置仍可保存**」。③ `dsh-memory doctor` 增报看门狗锁可写性(受限环境一眼可见)。`npm test` 共 **334 项** |
|
|
397
|
-
| **v0.10.24** | 2026-10-13 | **「同目标、不同做法」也能归到一起了(issue #36 第三块,闭环)**。前两版解决了「冗余合并」与「矛盾判别」,但最后一块 —— 同一目的、不同工作流/代码 —— 按正文相似度**根本抓不到**(「用 7z 压缩」「用 tar.gz 分发」「npm 包瘦身」字面差很远、目标却一样)。做法:**先让模型给每张卡抽一句「这张卡解决什么问题」,再按目标聚类** —— 目标抽取结果缓存在 `refine-goals.json`(同一张卡只抽一次,第二次扫描 0 次调用),聚类用并查集;命中后按「同一目标 N 张卡」成组给出,动作二选一:**都保留**(记两两决策,下次不再提示)或**合并这一组**(走现有 /merge,新卡进审核队列)。*全程只读,唯一写入是目标缓存*。入口:面板「整理 → 🎯 同目标不同做法」+ API `/refine-goals`、`/refine-goals-keep`;CLI `dsh-memory refine --goals --keep --path A --path B`(聚类本身需要模型,在面板跑)。`npm test` 共 **332 项** |
|
|
398
398
|
|
|
399
399
|
<details>
|
|
400
|
-
<summary>更早的更新日志(v0.10.
|
|
400
|
+
<summary>更早的更新日志(v0.10.24 及以前 · 共 51 版,点开查看)</summary>
|
|
401
401
|
|
|
402
402
|
| 版本 | 日期 | 关键改动 |
|
|
403
403
|
|---|---|---|
|
|
404
|
+
| **v0.10.24** | 2026-10-13 | **「同目标、不同做法」也能归到一起了(issue #36 第三块,闭环)**。前两版解决了「冗余合并」与「矛盾判别」,但最后一块 —— 同一目的、不同工作流/代码 —— 按正文相似度**根本抓不到**(「用 7z 压缩」「用 tar.gz 分发」「npm 包瘦身」字面差很远、目标却一样)。做法:**先让模型给每张卡抽一句「这张卡解决什么问题」,再按目标聚类** —— 目标抽取结果缓存在 `refine-goals.json`(同一张卡只抽一次,第二次扫描 0 次调用),聚类用并查集;命中后按「同一目标 N 张卡」成组给出,动作二选一:**都保留**(记两两决策,下次不再提示)或**合并这一组**(走现有 /merge,新卡进审核队列)。*全程只读,唯一写入是目标缓存*。入口:面板「整理 → 🎯 同目标不同做法」+ API `/refine-goals`、`/refine-goals-keep`;CLI `dsh-memory refine --goals --keep --path A --path B`(聚类本身需要模型,在面板跑)。`npm test` 共 **332 项** |
|
|
404
405
|
| **v0.10.23** | 2026-10-13 | **存量「注入垃圾卡」可一键清理(issue #36 第二刀)**。上一版修掉了**新生成**的注入垃圾卡(技能目录 / `<system-reminder>` 被当 user 消息 → 剥标签后仍被当知识沉淀),但**存量还在**:在真实库里一跑就找出 **32 张**「和对话毫无关系」的卡(`- \`code-review\`: Review the changes…` 这种目录行、`用户system-reminder-you-are-teammate-c…` 这种碎片)。新增 `findInjectedCards()`(只**提议**、不动数据;判据:含注入标签 / 技能目录条目占比 ≥30% / 剥离注入后只剩 <200 字 / 标题是碎片)与 `purgeInjected()`(**走回收站、可还原**,逐个报告失败)。入口:面板「整理 → 🧹 注入垃圾卡」(扫描存量 / 单张回收 / 全部回收)+ CLI `dsh-memory refine --junk [--purge --path P] [--json]`;API `/refine-junk`、`/refine-junk-purge`。`npm test` 共 **331 项** |
|
|
405
406
|
| **v0.10.22** | 2026-10-13 | **「矛盾扫描」:相似可以合并,矛盾必须人判断(issue #36 第一刀)**。现状是 `/optimize` 只按相似度(≥0.55)**盲合并** —— 而两张语义相近、结论互斥的卡相似度同样很高(实测 0.93),盲合并会把冲突悄悄「和解」掉,且 `mergeCards` 会**永久删除**源卡。新增 `lib/refine.js` + 客户端「整理 → 🧭 矛盾扫描」:① **宽召回**(≥0.30,上限放到 1.0,特意把近重复那一段纳入 —— 那正是盲合并最危险的区间);② 模型只判「重复 / 补充 / 矛盾 / 无关」并给一句冲突点 + 建议保留谁;③ **人拍板**:保留 A / 保留 B / 都保留 / 合并;**扫描绝不写库**,`keepA|keepB` 只把让位那张送**回收站**(可还原);④ 决策记进 `refine-decisions.json`,同一对不再重复打扰;⑤ **安全联锁**:被判为矛盾的对**绝不参与一键优化**的盲合并。另修一处冗余来源:技能目录(`- \`skill-name\`: …`)这类注入文本不再被抓成垃圾卡(实测库里已有这种卡)。API:`/refine`、`/refine-scan`、`/refine-apply`;CLI:`dsh-memory refine`(列候选不调模型,`--apply` 落裁决)。`npm test` 共 **329 项** |
|
|
406
407
|
| **v0.10.21** | 2026-10-13 | **「库不可用」从「静默运行」变成「看得清」(issue #33,按评审裁剪范围)**。① **打开即体检**:老实现执行完 `PRAGMA journal_mode=WAL` 就当成功(这条 PRAGMA 失败时**不抛错**,只把真实模式当一行返回),文件只读 / 目录不可写时插件会一路静默跑下去;现在打开后**读回真实 journal 模式 + 真写一次探针**(建表→删表,写主库而非 temp 库),结论记进 `dbStatus()`,不可写时立即把原因与排查方向打到日志。② **`/overview` 降级返回**:库/目录不可用时不再吐 500(500 会被探针、面板、脚本读成「这个端口坏了 / 不是我们的服务」,历史上正是重启风暴与端口漂移的入口),改成 200 + `degraded:true` + `dbError` + `db:{journalMode,writable,error}`,并保住 `vaultDir` / `version` 身份字段 —— 「这是我们、但库有问题」从此可以被分开表达。③ **新增 `dsh-memory doctor`**:库健康自检(真实 journal 模式 / 写探针 / 备份清单 / 恢复引导),带 `--json` 可脚本化;不可写或疑似损坏时给出**只引导、不动数据**的步骤(先停写者 → 复制现场留证 → 查备份 → 替换 → 复核)。**刻意不做**(评审结论,已写成测试守卫防止顺手加回):`busy_timeout`(`DatabaseSync` 是同步 API,撞锁会阻塞宿主事件循环)与 WAL→DELETE 自动降级(会**永久**改变库模式,且 rollback journal 并发更差、可能反过来加剧锁竞争)。`npm test` 共 **322 项** |
|
package/lib/api.js
CHANGED
|
@@ -18,7 +18,7 @@ import { listCards, countCards, readCard, search, searchAll, graph, graphAll, ov
|
|
|
18
18
|
import { compressExcerpt, DEFAULT_CAPTURE_MAX_TOKENS } from './capture.js'
|
|
19
19
|
import { buildDiagnostics } from './feedback.js'
|
|
20
20
|
import { getSetupStatus, runSetup, mcpAgentAction } from './setup.js'
|
|
21
|
-
import { backupDb, dbStatus, rebuildGuidance } from './db.js'
|
|
21
|
+
import { backupDb, dbStatus, rebuildGuidance, withDbRetry } from './db.js'
|
|
22
22
|
import { createHub } from './sse.js'
|
|
23
23
|
import { probeServedVersion, restartResident, currentPkgVersion, readWatchdogLock, isPidAlive, probeLockWritable } from './watchdog.js'
|
|
24
24
|
|
|
@@ -127,6 +127,12 @@ export function emptyNumericFields(patch, dict = {}) {
|
|
|
127
127
|
.map(([k]) => k)
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* **真的会写库**的路由(/card、/cards、/search 这些是读,绝不能拦)。
|
|
132
|
+
* 用途:库不可写时短路径失败;撞锁(SQLITE_BUSY)时做应用层异步重试。
|
|
133
|
+
*/
|
|
134
|
+
const DB_WRITE_ROUTES = /^\/(delete|merge|write|import|refine-apply|refine-goals-keep|refine-junk-purge|optimize-execute|feedback|audit\/(approve|reject)|recycle\/(restore|purge|purge-expired)|backup)$/
|
|
135
|
+
|
|
130
136
|
export function createApi(deps = {}) {
|
|
131
137
|
const vaultDir = deps.vaultDir ?? (() => '')
|
|
132
138
|
const vaultRoots = deps.vaultRoots ?? (() => [{ name: '', root: vaultDir() }])
|
|
@@ -158,8 +164,6 @@ export function createApi(deps = {}) {
|
|
|
158
164
|
// 库不可写时**短路径失败**(issue #33):这些路由一定会写库,早失败早说清,避免做一半、
|
|
159
165
|
// 也避免每个接口各自把 `attempt to write a readonly database` 原文丢给用户。
|
|
160
166
|
// **只读路由与配置路由不受影响**(配置不经过数据库 —— 用户至少还能改设置)。
|
|
161
|
-
// 只列**真的会写库**的路由(/card、/cards、/search 这些是读,绝不能拦)
|
|
162
|
-
const DB_WRITE_ROUTES = /^\/(delete|merge|write|import|write-card|refine-apply|refine-goals-keep|refine-junk-purge|optimize-execute|feedback|audit\/(approve|reject)|recycle\/(restore|purge|purge-expired)|backup)$/
|
|
163
167
|
if (DB_WRITE_ROUTES.test(route)) {
|
|
164
168
|
const st = dbStatus(vaultRoot)
|
|
165
169
|
if (st && st.writable === false) {
|
|
@@ -903,7 +907,21 @@ export function createApi(deps = {}) {
|
|
|
903
907
|
* `attempt to write a readonly database` 原文对用户毫无意义),并带上恢复指引。
|
|
904
908
|
*/
|
|
905
909
|
return async function handleApi(req, res) {
|
|
910
|
+
const routePath = (() => { try { return new URL(req.url, 'http://localhost').pathname.slice(API_PREFIX.length).replace(/\/+$/, '') || '/overview' } catch { return '' } })()
|
|
911
|
+
const isWriteRoute = DB_WRITE_ROUTES.test(routePath)
|
|
906
912
|
try {
|
|
913
|
+
// 写路由:撞锁(SQLITE_BUSY / database is locked)做**应用层异步重试** ——
|
|
914
|
+
// 不阻塞事件循环(`busy_timeout` 是同步忙等,评审已否),且只在**还没发出响应**时重试,
|
|
915
|
+
// 避免重复写;SQLite 在 BUSY 时事务已回滚,重试是安全的。
|
|
916
|
+
if (isWriteRoute) {
|
|
917
|
+
return await withDbRetry(async () => {
|
|
918
|
+
try { return await dispatch(req, res) } catch (e) {
|
|
919
|
+
// 响应已经发出(writeHead 过了)→ 不能再重试,否则可能写两遍
|
|
920
|
+
if (res && res.headersSent) { e.noRetry = true; throw e }
|
|
921
|
+
throw e
|
|
922
|
+
}
|
|
923
|
+
}, { tries: 3, delayMs: 120 })
|
|
924
|
+
}
|
|
907
925
|
return await dispatch(req, res)
|
|
908
926
|
} catch (e) {
|
|
909
927
|
const msg = String((e && e.message) || e)
|
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,f=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&o(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch($+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return f()}).catch(()=>r?f():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[f,c]=(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:()=>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)(Qt,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),f&&(0,e.jsx)(Ta,{t,locale:o,onClose:()=>c(!1)})]})}function Ta({t,locale:o,onClose:r,tab:f}){let[c,i]=(0,n.useState)(!1),w=(0,n.useRef)(null),z=Ca(),k=(0,n.useCallback)(B=>o&&typeof o.subscribe=="function"?o.subscribe(B):()=>{},[o]),F=(0,n.useCallback)(()=>o&&typeof o.getSnapshot=="function"?o.getSnapshot():null,[o]),N=(0,n.useSyncExternalStore)(k,F),h=N&&(N.active==="zh"||N.active==="en")?N.active:null,g=(0,n.useRef)(null);g.current===null&&h&&(g.current=h);let L=(0,n.useMemo)(()=>{let B=g.current?`lang=${g.current}`:"";if(f==="config")return`${$}/ui/config?tab=config${B?`&${B}`:""}`;let x=z.replace(/\/$/,""),S=x.endsWith("/ui/app")?"?":"/?";return f?`${x}${S}tab=${f}${B?`&${B}`:""}`:B?`${x}${S}${B}`:z},[z,f]);return(0,n.useEffect)(()=>{let B=w.current&&w.current.contentWindow;B&&h&&B.postMessage({source:"memory-eternal",type:"locale",lang:h},"*")},[h]),(0,n.useEffect)(()=>{let B=x=>{x.key==="Escape"&&r()};return window.addEventListener("keydown",B),()=>window.removeEventListener("keydown",B)},[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"+(c?" me-modal-full":""),onClick:B=>B.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(B=>!B),"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:w,src:L,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,f=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&o(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch($+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return f()}).catch(()=>r?f():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[f,c]=(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:()=>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)(Qt,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),f&&(0,e.jsx)(Ta,{t,locale:o,onClose:()=>c(!1)})]})}function Ta({t,locale:o,onClose:r,tab:f}){let[c,i]=(0,n.useState)(!1),w=(0,n.useRef)(null),z=Ca(),k=(0,n.useCallback)(B=>o&&typeof o.subscribe=="function"?o.subscribe(B):()=>{},[o]),F=(0,n.useCallback)(()=>o&&typeof o.getSnapshot=="function"?o.getSnapshot():null,[o]),N=(0,n.useSyncExternalStore)(k,F),h=N&&(N.active==="zh"||N.active==="en")?N.active:null,g=(0,n.useRef)(null);g.current===null&&h&&(g.current=h);let L=(0,n.useMemo)(()=>{let B=g.current?`lang=${g.current}`:"";if(f==="config")return`${$}/ui/config?tab=config${B?`&${B}`:""}`;let x=z.replace(/\/$/,""),S=x.endsWith("/ui/app")?"?":"/?";return f?`${x}${S}tab=${f}${B?`&${B}`:""}`:B?`${x}${S}${B}`:z},[z,f]);return(0,n.useEffect)(()=>{let B=w.current&&w.current.contentWindow;B&&h&&B.postMessage({source:"memory-eternal",type:"locale",lang:h},"*")},[h]),(0,n.useEffect)(()=>{let B=x=>{x.key==="Escape"&&r()};return window.addEventListener("keydown",B),()=>window.removeEventListener("keydown",B)},[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"+(c?" me-modal-full":""),onClick:B=>B.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(B=>!B),"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:w,src:L,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.26";function Ra({t,inModal:o,onClose:r,onFull:f,full:c}){let[i,w]=(0,n.useState)(null),[z,k]=(0,n.useState)([]),[F,N]=(0,n.useState)(!0),[h,g]=(0,n.useState)(null),[L,B]=(0,n.useState)(""),[x,S]=(0,n.useState)("all"),[v,_]=(0,n.useState)(""),[Q,Ce]=(0,n.useState)("all"),[G,Be]=(0,n.useState)("all"),[Y,me]=(0,n.useState)(()=>{try{let u=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),I={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(u&&I[u])return I[u]}catch{}return"cards"}),[de,H]=(0,n.useState)(()=>new Set([Y])),Z=(0,n.useCallback)(u=>{H(I=>I.has(u)?I:new Set(I).add(u)),me(u)},[]),[ve,M]=(0,n.useState)("recent"),[Ne,J]=(0,n.useState)(null),se=(0,n.useRef)(null),Le=(0,n.useRef)(null),[y,q]=(0,n.useState)(!1),[re,X]=(0,n.useState)(null),[ne,ye]=(0,n.useState)(!1),[l,C]=(0,n.useState)(!1),[T,V]=(0,n.useState)(!0),[we,ge]=(0,n.useState)(null),[oe,Se]=(0,n.useState)(0),Ye=(0,n.useCallback)(()=>Se(u=>u+1),[]),Ge=i?.quarantine?.pending??0,Ue=i?.quarantine?.deleted??0,pe=(0,n.useRef)(null),[Xe,Te]=(0,n.useState)(0),[p,s]=(0,n.useState)(!1),O=(0,n.useRef)(null),ie=(0,n.useRef)(null),le=(0,n.useCallback)(async(u=x,I=v,D=G,U={})=>{let W=U.append===!0;try{W?s(!0):(N(!0),B(""));let ue=new URLSearchParams;u&&u!=="all"&&ue.set("kind",u),I.trim()&&ue.set("q",I.trim()),ue.set("status",D||"all"),ue.set("limit",String(va)),ue.set("offset",String(U.offset||0)),ve==="title"&&ue.set("sort","title");let Ie=await(await fetch(`${$}/cards?${ue.toString()}`)).json();if(Ie.ok){let Ke=Ie.cards||[];k(Ee=>W?[...Ee,...Ke.filter(qe=>!Ee.some(vt=>vt.path===qe.path))]:Ke),Te(Number(Ie.total)||Ke.length)}}catch(ue){B(String(ue&&ue.message?ue.message:ue))}finally{W?s(!1):N(!1)}},[x,v,G,ve]),fe=(0,n.useCallback)(async()=>{try{let[u,I]=await Promise.all([fetch(`${$}/overview`).then(D=>D.json()),fetch(`${$}/capture-log`).then(D=>D.json()).catch(()=>null)]);u.ok&&w(u),g(I&&I.ok&&I.available&&I.health||null)}catch(u){B(String(u&&u.message?u.message:u))}finally{N(!1)}},[]),a=async u=>{try{let D=await(await fetch(`${$}/delete?path=${encodeURIComponent(u)}`)).json();D.ok?(J({ok:!0,msg:t("deleted")}),await fe(),Ye()):J({ok:!1,msg:D.error||t("deleteFail")})}catch{J({ok:!1,msg:t("deleteFail")})}se.current&&clearTimeout(se.current),se.current=setTimeout(()=>J(null),2e3)};(0,n.useEffect)(()=>(fe(),()=>{pe.current&&clearTimeout(pe.current)}),[fe]);let tt=u=>{_(u),pe.current&&clearTimeout(pe.current),pe.current=setTimeout(()=>le(x,u,G),280)},ke=u=>{S(u),le(u,v,G)};(0,n.useEffect)(()=>{Y==="cards"&&le(x,v,G)},[G,Y,oe,ve]),(0,n.useEffect)(()=>{if(Y!=="cards"||F||p||z.length>=Xe)return;let u=O.current,I=ie.current;if(!u)return;let D=new IntersectionObserver(U=>{U[0]&&U[0].isIntersecting&&le(x,v,G,{append:!0,offset:z.length})},{root:I||null,rootMargin:"400px"});return D.observe(u),()=>D.disconnect()},[Y,z,Xe,F,p,x,v,G,le]);let nt=async u=>{try{let I=u.status;if(!I&&u.path)try{let Re=((await fetch(`${$}/cards?status=all&q=${encodeURIComponent(u.title||"")}&limit=50`).then(Ie=>Ie.json())).cards||[]).find(Ie=>Ie.path===u.path);Re&&(I=Re.status)}catch{}let D=I&&I!=="approved"?`&status=${encodeURIComponent(I)}`:"",U=await fetch(`${$}/card?path=${encodeURIComponent(u.path||u.id)}${D}`),W=await U.json();W.ok?X({path:W.path,title:u.title,text:W.text}):B(W.error||`HTTP ${U.status}`)}catch(I){B(String(I&&I.message?I.message:I))}},ze=async(u,I)=>{q(!0);try{let D=await fetch(`${$}/export`);if(!D.ok)throw new Error("HTTP "+D.status);let U=await D.json();if(!U.ok)throw new Error(U.error||"export failed");let W,ue,Re;if(u==="json"){ue="memory-vault.json",Re="application/json";let Ee=U.cards.map(qe=>({path:qe.path,title:qe.title,kind:qe.kind,status:qe.status,store:qe.store,text:qe.text}));W=JSON.stringify({format:U.format||"memory-eternal-vault",formatVersion:U.formatVersion||1,exportedAt:U.exportedAt||new Date().toISOString(),count:Ee.length,cards:Ee},null,2)}else ue="memory-vault.md",Re="text/markdown;charset=utf-8",W=U.cards.map(Ee=>Ee.text.trim()).filter(Boolean).join(`
|
|
302
302
|
|
|
303
303
|
---
|
|
304
304
|
|
package/lib/db.js
CHANGED
|
@@ -53,6 +53,38 @@ export function checkDbHealth(db) {
|
|
|
53
53
|
return out
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** 这个错误是不是「库被别人锁着」(SQLite 的 BUSY / locked),而不是数据或语法问题。 */
|
|
57
|
+
export function isDbBusyError(e) {
|
|
58
|
+
const m = String((e && e.message) || e)
|
|
59
|
+
return /database is locked|database table is locked|SQLITE_BUSY/i.test(m)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 撞锁时**在应用层异步重试**(issue #31/#33 的实测补充)。
|
|
64
|
+
*
|
|
65
|
+
* 为什么不用 `PRAGMA busy_timeout`:`node:sqlite` 的 `DatabaseSync` 是**同步 API**,
|
|
66
|
+
* 忙等会阻塞宿主的事件循环(整个 DSH 卡住)—— 评审后**故意没做**。这里改成:失败 → 让出事件循环
|
|
67
|
+
* 睡一小会儿 → 再试,最多几次。SQLite 在 BUSY 时不会留下半成品(事务回滚),所以重试是安全的;
|
|
68
|
+
* 实测现场:外部进程持写事务时我方立刻拿到 `database is locked`,且对方被强杀后**仍然锁着** ——
|
|
69
|
+
* 这种瞬时锁正好由短退避重试化解。
|
|
70
|
+
*
|
|
71
|
+
* @param {() => Promise<any>} fn
|
|
72
|
+
* @param {{tries?:number, delayMs?:number}} opts
|
|
73
|
+
*/
|
|
74
|
+
export async function withDbRetry(fn, { tries = 3, delayMs = 120 } = {}) {
|
|
75
|
+
const n = Math.max(1, Number(tries) || 3)
|
|
76
|
+
let last
|
|
77
|
+
for (let i = 0; i < n; i++) {
|
|
78
|
+
try { return await fn() } catch (e) {
|
|
79
|
+
last = e
|
|
80
|
+
if (e && e.noRetry) throw e // 调用方显式禁止重试(例如响应已发出)
|
|
81
|
+
if (!isDbBusyError(e) || i === n - 1) throw e
|
|
82
|
+
await new Promise((r) => setTimeout(r, (Number(delayMs) || 120) * (i + 1))) // 线性退避,异步让出
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw last
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
/** 某个库最近一次打开时的健康结论(没打开过返回 null)。 */
|
|
57
89
|
export function dbStatus(root) {
|
|
58
90
|
return healthByRoot.get(path.resolve(root)) || null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.26",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
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.26";function ef({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,v.useState)(null),[s,u]=(0,v.useState)([]),[p,h]=(0,v.useState)(!0),[y,m]=(0,v.useState)(null),[b,T]=(0,v.useState)(""),[w,D]=(0,v.useState)("all"),[c,d]=(0,v.useState)(""),[g,z]=(0,v.useState)("all"),[R,U]=(0,v.useState)("all"),[P,F]=(0,v.useState)(()=>{try{let N=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),W={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(N&&W[N])return W[N]}catch{}return"cards"}),[q,_]=(0,v.useState)(()=>new Set([P])),te=(0,v.useCallback)(N=>{_(W=>W.has(N)?W:new Set(W).add(N)),F(N)},[]),[Ce,O]=(0,v.useState)("recent"),[Be,le]=(0,v.useState)(null),ge=(0,v.useRef)(null),dt=(0,v.useRef)(null),[L,ne]=(0,v.useState)(!1),[me,oe]=(0,v.useState)(null),[ce,Ne]=(0,v.useState)(!1),[k,j]=(0,v.useState)(!1),[A,re]=(0,v.useState)(!0),[Fe,ke]=(0,v.useState)(null),[de,Oe]=(0,v.useState)(0),qt=(0,v.useCallback)(()=>Oe(N=>N+1),[]),Zt=l?.quarantine?.pending??0,At=l?.quarantine?.deleted??0,xe=(0,v.useRef)(null),[Jt,$e]=(0,v.useState)(0),[C,x]=(0,v.useState)(!1),G=(0,v.useRef)(null),pe=(0,v.useRef)(null),ye=(0,v.useCallback)(async(N=w,W=c,H=R,J={})=>{let X=J.append===!0;try{X?x(!0):(h(!0),T(""));let we=new URLSearchParams;N&&N!=="all"&&we.set("kind",N),W.trim()&&we.set("q",W.trim()),we.set("status",H||"all"),we.set("limit",String(Wg)),we.set("offset",String(J.offset||0)),Ce==="title"&&we.set("sort","title");let it=await(await fetch(`${Z}/cards?${we.toString()}`)).json();if(it.ok){let Bt=it.cards||[];u(Et=>X?[...Et,...Bt.filter(Ht=>!Et.some(Go=>Go.path===Ht.path))]:Bt),$e(Number(it.total)||Bt.length)}}catch(we){T(String(we&&we.message?we.message:we))}finally{X?x(!1):h(!1)}},[w,c,R,Ce]),be=(0,v.useCallback)(async()=>{try{let[N,W]=await Promise.all([fetch(`${Z}/overview`).then(H=>H.json()),fetch(`${Z}/capture-log`).then(H=>H.json()).catch(()=>null)]);N.ok&&a(N),m(W&&W.ok&&W.available&&W.health||null)}catch(N){T(String(N&&N.message?N.message:N))}finally{h(!1)}},[]),f=async N=>{try{let H=await(await fetch(`${Z}/delete?path=${encodeURIComponent(N)}`)).json();H.ok?(le({ok:!0,msg:e("deleted")}),await be(),qt()):le({ok:!1,msg:H.error||e("deleteFail")})}catch{le({ok:!1,msg:e("deleteFail")})}ge.current&&clearTimeout(ge.current),ge.current=setTimeout(()=>le(null),2e3)};(0,v.useEffect)(()=>(be(),()=>{xe.current&&clearTimeout(xe.current)}),[be]);let Sn=N=>{d(N),xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>ye(w,N,R),280)},De=N=>{D(N),ye(N,c,R)};(0,v.useEffect)(()=>{P==="cards"&&ye(w,c,R)},[R,P,de,Ce]),(0,v.useEffect)(()=>{if(P!=="cards"||p||C||s.length>=Jt)return;let N=G.current,W=pe.current;if(!N)return;let H=new IntersectionObserver(J=>{J[0]&&J[0].isIntersecting&&ye(w,c,R,{append:!0,offset:s.length})},{root:W||null,rootMargin:"400px"});return H.observe(N),()=>H.disconnect()},[P,s,Jt,p,C,w,c,R,ye]);let Kn=async N=>{try{let W=N.status;if(!W&&N.path)try{let Qe=((await fetch(`${Z}/cards?status=all&q=${encodeURIComponent(N.title||"")}&limit=50`).then(it=>it.json())).cards||[]).find(it=>it.path===N.path);Qe&&(W=Qe.status)}catch{}let H=W&&W!=="approved"?`&status=${encodeURIComponent(W)}`:"",J=await fetch(`${Z}/card?path=${encodeURIComponent(N.path||N.id)}${H}`),X=await J.json();X.ok?oe({path:X.path,title:N.title,text:X.text}):T(X.error||`HTTP ${J.status}`)}catch(W){T(String(W&&W.message?W.message:W))}},et=async(N,W)=>{ne(!0);try{let H=await fetch(`${Z}/export`);if(!H.ok)throw new Error("HTTP "+H.status);let J=await H.json();if(!J.ok)throw new Error(J.error||"export failed");let X,we,Qe;if(N==="json"){we="memory-vault.json",Qe="application/json";let Et=J.cards.map(Ht=>({path:Ht.path,title:Ht.title,kind:Ht.kind,status:Ht.status,store:Ht.store,text:Ht.text}));X=JSON.stringify({format:J.format||"memory-eternal-vault",formatVersion:J.formatVersion||1,exportedAt:J.exportedAt||new Date().toISOString(),count:Et.length,cards:Et},null,2)}else we="memory-vault.md",Qe="text/markdown;charset=utf-8",X=J.cards.map(Et=>Et.text.trim()).filter(Boolean).join(`
|
|
304
304
|
|
|
305
305
|
---
|
|
306
306
|
|