memory-eternal 0.9.0 → 0.9.1
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/README.en.md +11 -0
- package/README.md +9 -0
- package/index.js +12 -3
- package/lib/api.js +77 -11
- package/lib/client.js +5 -5
- package/lib/vault.js +132 -25
- package/lib/watchdog.js +28 -4
- package/lib/web.js +8 -3
- package/package.json +1 -1
- package/web/app.js +9 -9
package/README.en.md
CHANGED
|
@@ -37,6 +37,14 @@ dsh plugin --profile web add memory-eternal
|
|
|
37
37
|
|
|
38
38
|
# or update directly in the profile (pnpm workspace). Use pnpm — `npm install` throws EUNSUPPORTEDPROTOCOL
|
|
39
39
|
cd ~/.dsh/profiles/web && pnpm add memory-eternal@latest
|
|
40
|
+
|
|
41
|
+
# right after a release, @latest can be held back by pnpm's minimum-release-age gate or a cached
|
|
42
|
+
# packument (it silently stays on the old version): pin the version instead — pnpm adds it to the
|
|
43
|
+
# workspace's minimumReleaseAgeExclude automatically
|
|
44
|
+
cd ~/.dsh/profiles/web && pnpm add memory-eternal@0.9.0
|
|
45
|
+
|
|
46
|
+
# verify the version, then restart dsh web
|
|
47
|
+
node -e "console.log(require('memory-eternal/package.json').version)"
|
|
40
48
|
```
|
|
41
49
|
|
|
42
50
|
After **restarting dsh web**, three things are live immediately:
|
|
@@ -182,6 +190,8 @@ Auto-capture is a silent background pipeline: when it broke, the only symptom us
|
|
|
182
190
|
|
|
183
191
|
## 🛡️ Audit Center & Recycle Bin
|
|
184
192
|
|
|
193
|
+
> **Agents cannot approve on your behalf (since v0.9.1).** Whenever audit is active, the plugin injects a red line into the agent context: new cards stay `pending`, and calling the `approve`/`reject` endpoints (or any equivalent bypass) is forbidden. The decision is always yours.
|
|
194
|
+
|
|
185
195
|
New cards go to the **Audit Center** (`pending`) by default and enter the main vault only after you approve them; rejected ones move to "Rejected", where you can restore or delete to the recycle bin. Cards matching an exemption (audit mode = skip all / exempt agents / exempt kinds) go straight in; recycle-bin cards are recoverable within 30 days, then auto-purged.
|
|
186
196
|
|
|
187
197
|
<p align="center">
|
|
@@ -236,6 +246,7 @@ npm run build # builds lib/client.js (DSH embed) + web/app.js (standalone web
|
|
|
236
246
|
|
|
237
247
|
| Version | Date | Highlights |
|
|
238
248
|
|---|---|---|
|
|
249
|
+
| **v0.9.1** | 2026-09-12 | **Performance & UX batch**: memory page loads lazily per view (opening Settings no longer drags in 250KB of cards + 890KB of graph; first paint needs ~0.5KB); graph generation **3232ms → 43ms** (bigram sets computed once + lossless mathematical-bound pruning + tag inverted index) plus a server-side cache with **fingerprint invalidation** (write/audit/delete rebuilds immediately, no stale reads); site-wide **brotli q5** (graph 877KB → **54KB**, 34% smaller than gzip; cards 246KB → 83KB → 20KB with br) and large responses compressed with both br and gzip keeping the smaller, so no response is ever worse than gzip; `/cards` gained **real pagination** (offset/limit/total + server-side title sort, globally ordered across pages) loading 100 cards first and fetching more on scroll; the "Agent cards" panel in Usage/Today filters by agent server-side (30 rows); **audit red line**: the plugin injects "never call approve/reject on the user's behalf" into the agent context; the watchdog now **reaps redundant instances on fallback ports** (a duplicate web process holding 76MB was reaped in testing) |
|
|
239
250
|
| **v0.9.0** | 2026-09-12 | **Fixed silent auto-capture death (6 days without a single card)**: after a DSH upgrade the session event API moved from `events` to `ownEvents()/snapshotEvents()`; the listener read nothing, returned silently and left no trace — now it tries `ownEvents()` → `snapshotEvents()` → `events` and logs a per-session attach entry. Daily quota now counts **real writes only** (previously every "not worth saving" verdict burned quota, exhausting the 24h budget after 40 attempts); `captureCooldownMs` actually applies; a single capture input is tail-capped at 20k chars. New **capture log** panel and **active failure alerts** (red strip + agent-context injection + turn-stopping stall sweep). Removed the automatic "Daily review" digest (timer / `/todayBrief` route / UI button). |
|
|
240
251
|
| **v0.8.0** | 2026-09-06 | **Full UI i18n**: follows the DSH system language live (zh/en) across cards, graph, audit center, config panel and new-card templates; SQLite EBUSY fix for the Windows test suite |
|
|
241
252
|
| **v0.3.1** | 2026-09-03 | Knowledge graph loads on demand (fetch on open, unregister on close) |
|
package/README.md
CHANGED
|
@@ -37,6 +37,13 @@ dsh plugin --profile web add memory-eternal
|
|
|
37
37
|
|
|
38
38
|
# 或直接在 profile(pnpm workspace)里更新。注意用 pnpm,npm install 会报 EUNSUPPORTEDPROTOCOL
|
|
39
39
|
cd ~/.dsh/profiles/web && pnpm add memory-eternal@latest
|
|
40
|
+
|
|
41
|
+
# 刚发新版时 @latest 可能被 pnpm 的最小发布年龄门槛/元数据缓存挡住(静默停在旧版):
|
|
42
|
+
# 直接指定版本号最快,pnpm 会自动把它加进 workspace 的 minimumReleaseAgeExclude
|
|
43
|
+
cd ~/.dsh/profiles/web && pnpm add memory-eternal@0.9.0
|
|
44
|
+
|
|
45
|
+
# 验版本(应等于上面装的号),然后重启 dsh web 生效
|
|
46
|
+
node -e "console.log(require('memory-eternal/package.json').version)"
|
|
40
47
|
```
|
|
41
48
|
|
|
42
49
|
**重启 dsh web** 后,三样东西立即生效:
|
|
@@ -204,6 +211,7 @@ dsh-memory watchdog [--port 7799] # 看门狗保活 web(独立进程)
|
|
|
204
211
|
|
|
205
212
|
- **待审核 / 已驳回** 双页签,按类型 / 日期 / 智能体筛选,全选后一键「批准 / 驳回 / 删除进回收站」。
|
|
206
213
|
- 审核规则在「记忆配置 → 自动审核配置」:`审核模式`(全部要审 / 全部免审)+ `免审智能体` + `免审类型` + `回收保留天数`。
|
|
214
|
+
- **Agent 不能代你审批(v0.9.1 起)**:只要审核生效,插件就往 Agent 的上下文里注入红线——新卡只能停在「待审核」,并明确禁止调用 `approve`/`reject` 接口或任何等价方式绕过;判断权始终在你手里。
|
|
207
215
|
|
|
208
216
|
### 🥇 为什么审核系统更靠谱(对比其它记忆产品)
|
|
209
217
|
|
|
@@ -251,6 +259,7 @@ npm run build # 构建 lib/client.js(DSH 内嵌)+ web/app.js(独立 web
|
|
|
251
259
|
|
|
252
260
|
| 版本 | 日期 | 关键改动 |
|
|
253
261
|
|---|---|---|
|
|
262
|
+
| **v0.9.1** | 2026-09-12 | **性能与体验批次**:记忆页按视图**懒加载**(打开「设置」不再顺带拉 250KB 卡片 + 890KB 图谱,首屏只需约 0.5KB);知识图谱生成 **3232ms → 43ms**(bigram 集合只算一次 + 数学上界剪枝无损跳过 + 标签倒排),并加**指纹精确失效**的服务端缓存(写卡/审核/删卡立即重建,无脏读);全站 **brotli q5**(图谱 877KB → **54KB**,比 gzip 再小 34%;卡片 246KB → 83KB → br 20KB),大响应体同时压 br + gzip 取更小者,保证任何响应都不比 gzip 差;`/cards` 支持**真分页**(offset/limit/total + 服务端标题排序,跨页全局有序),首屏只拉 100 条、滚到底续拉;「用量/今日」的 Agent 卡清单改为服务端署名过滤(30 条);**审核红线**:插件在 Agent 上下文注入「禁止调用 approve/reject 代用户审批」;watchdog **回收备用端口的多余实例**(实测清掉一个常驻 76MB 的重复 web 进程) |
|
|
254
263
|
| **v0.9.0** | 2026-09-12 | **修复自动沉淀静默失效(一连 6 天不写卡)**:DSH 升级后会话事件接口由 `events` 改为 `ownEvents()/snapshotEvents()`,监听器读不到事件后静默返回、不留任何痕迹 —— 现改为三级自适应取法(`ownEvents()` → `snapshotEvents()` → `events`)+ 每会话接入留痕;日配额改为**只统计真实写卡**(此前「不值得保存」的判定也照样扣配额,40 次即耗尽 24h 配额)、`captureCooldownMs` 冷却真正生效、单次沉淀输入截尾 20000 字;新增**自动沉淀日志**面板与**异常主动提示**(页面红条 + 注入 Agent 上下文 + 轮次收尾停滞巡检);移除「每日回顾」自动简报(定时器 / `/todayBrief` 接口 / UI 按钮) |
|
|
255
264
|
| **v0.8.0** | 2026-09-06 | **全套 UI i18n**:跟随 DSH 系统语言实时切换(中/英),覆盖知识卡 / 知识图谱 / 审核中心 / 配置面板 / 新建卡模板;Windows 测试收尾 SQLite 连接 EBUSY 修复 |
|
|
256
265
|
| **v0.3.1** | 2026-09-03 | 知识图谱按需加载(点击时 fetch,关闭时注销) |
|
package/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
28
28
|
import { ensureVault, search } from './lib/vault.js'
|
|
29
29
|
import { migrateFromMarkdown, setAuditConfig, backupDb } from './lib/db.js'
|
|
30
30
|
import { summarizeTurn, extractLastTurn, sliceNewEvents, sessionEvents, sessionEventApi, createCaptureHealth, resolveRoute, captureCard, captureUpdate, pickNeighbors } from './lib/capture.js'
|
|
31
|
-
import { createApi, json } from './lib/api.js'
|
|
31
|
+
import { createApi, json, encodeBody } from './lib/api.js'
|
|
32
32
|
|
|
33
33
|
export const name = 'memory-eternal'
|
|
34
34
|
export const inject = ['systemPrompt', 'settings']
|
|
@@ -381,6 +381,10 @@ export function apply(ctx, config) {
|
|
|
381
381
|
'3. 若检索结果为空,就诚实说明当前记忆库没有相关内容,不要编造。',
|
|
382
382
|
'4. 知识卡存储在 SQLite 数据库中(memory-eternal.db),**禁止**用文件工具直接读写 vault 目录下的任何文件。沉淀记忆必须通过 memory_recall 工具或 /memory-eternal/api/write API。',
|
|
383
383
|
'5. 若下方出现「自动沉淀异常」,必须在本次回复的第一句用中文转述该异常并提醒用户处理,不要自行猜测或尝试修复。',
|
|
384
|
+
// 审核红线(主上 2026-09-12 强制要求):只在审核真正生效时注入。
|
|
385
|
+
...((cfg.auditMode ?? 'all') === 'none' ? [] : [
|
|
386
|
+
'6. **审核红线(强制)**:写卡一律停在 pending,由用户在「审核中心」审批。**禁止**调用 /memory-eternal/api/audit/approve 或 /audit/reject 代替用户审批,也不得用任何等价方式绕过审核;用户说「记录一下 / 增加记忆」不等于允许免审入库。需要立刻可用时,写卡后明确告知用户「已进审核中心,待批准」。',
|
|
387
|
+
]),
|
|
384
388
|
].join('\n')
|
|
385
389
|
// 异常提示:沉淀管线坏了,靠这一句把消息送到用户面前(不依赖用户去翻页面)。
|
|
386
390
|
const h = health.snapshot()
|
|
@@ -600,8 +604,13 @@ export function apply(ctx, config) {
|
|
|
600
604
|
const webRoot = path.join(PACKAGE_ROOT, 'web')
|
|
601
605
|
if (pathname.endsWith('app.js')) {
|
|
602
606
|
const buf = await readFile(path.join(webRoot, 'app.js'))
|
|
603
|
-
|
|
604
|
-
|
|
607
|
+
// client bundle 270KB,每次打开配置页都要重下 —— 支持 gzip 的客户端走压缩
|
|
608
|
+
const headers = { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-store', 'Vary': 'Accept-Encoding' }
|
|
609
|
+
const { body: out, encoding } = encodeBody(res, buf)
|
|
610
|
+
if (encoding) headers['Content-Encoding'] = encoding
|
|
611
|
+
headers['Content-Length'] = out.length
|
|
612
|
+
res.writeHead(200, headers)
|
|
613
|
+
return res.end(out)
|
|
605
614
|
}
|
|
606
615
|
const buf = await readFile(path.join(webRoot, 'index.html'))
|
|
607
616
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' })
|
package/lib/api.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// - getSettings() → 插件设置对象(/budget /compress 读取;可选,缺省 {})
|
|
12
12
|
// - getCaptureLog() → 自动沉淀运行轨迹(DSH host 注入;独立 web server 无此数据)
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib'
|
|
15
|
+
import { listCards, countCards, readCard, search, searchAll, graph, graphAll, overview, exportCards, deleteCard, writeCard, parseCard, stats, optimizeCandidates, optimizeApply, addFeedback, dailyCounts, mergeCards, ensureVault, auditQueue, recycleList, purgeExpired, setCardStatus, restoreCard } from './vault.js'
|
|
15
16
|
import { compressExcerpt } from './capture.js'
|
|
16
17
|
import { getSetupStatus, runSetup, mcpAgentAction } from './setup.js'
|
|
17
18
|
import { backupDb } from './db.js'
|
|
@@ -44,17 +45,25 @@ export function createApi(deps = {}) {
|
|
|
44
45
|
const kind = query.get('kind') || ''
|
|
45
46
|
const q = query.get('q') || ''
|
|
46
47
|
const status = (query.get('status') || 'approved').toLowerCase()
|
|
47
|
-
|
|
48
|
+
// 真分页:offset/limit + total(客户端据此判断还有没有下一页)
|
|
49
|
+
const limit = Math.min(Math.max(Number(query.get('limit')) || 200, 1), 500)
|
|
50
|
+
const offset = Math.max(Number(query.get('offset')) || 0, 0)
|
|
51
|
+
const sort = query.get('sort') === 'title' ? 'title' : 'recent'
|
|
52
|
+
const agent = query.get('agent') || '' // 服务端署名过滤(如 agent=deepseek-harness)
|
|
48
53
|
// all = 全部(含 pending/rejected,排除回收站);默认/approved = 已审核。
|
|
49
54
|
const want = status === 'all' ? ['approved', 'pending', 'rejected'] : status === 'pending' || status === 'rejected' ? [status] : ['approved']
|
|
50
|
-
let cards = await listCards(vaultRoot, { status: want })
|
|
51
|
-
if (kind) cards = cards.filter((c) => c.kind === kind)
|
|
52
55
|
if (q.trim()) {
|
|
53
|
-
|
|
56
|
+
// 检索是相关性排序:先把命中集合取齐,再在内存里分页(保证 total 与顺序一致)
|
|
57
|
+
const hits = await search(vaultRoot, q, { limit: 500 })
|
|
54
58
|
const hitPaths = new Set(hits.map((h) => h.path))
|
|
55
|
-
cards =
|
|
59
|
+
let cards = (await listCards(vaultRoot, { status: want, kind, agent })).filter((c) => hitPaths.has(c.path))
|
|
60
|
+
const total = cards.length
|
|
61
|
+
json(res, 200, { ok: true, cards: cards.slice(offset, offset + limit), total, offset, limit })
|
|
62
|
+
return
|
|
56
63
|
}
|
|
57
|
-
|
|
64
|
+
const total = await countCards(vaultRoot, { status: want, kind, agent })
|
|
65
|
+
const cards = await listCards(vaultRoot, { status: want, kind, agent, limit, offset, sort })
|
|
66
|
+
json(res, 200, { ok: true, cards, total, offset, limit })
|
|
58
67
|
return
|
|
59
68
|
}
|
|
60
69
|
case '/card': {
|
|
@@ -317,12 +326,69 @@ export function createApi(deps = {}) {
|
|
|
317
326
|
|
|
318
327
|
export function json(res, status, body) {
|
|
319
328
|
const payload = JSON.stringify(body)
|
|
320
|
-
|
|
329
|
+
const headers = {
|
|
321
330
|
'Content-Type': 'application/json; charset=utf-8',
|
|
322
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
323
331
|
'Cache-Control': 'no-store',
|
|
324
|
-
|
|
325
|
-
|
|
332
|
+
'Vary': 'Accept-Encoding',
|
|
333
|
+
}
|
|
334
|
+
const { body: out, encoding } = encodeBody(res, Buffer.from(payload, 'utf8'))
|
|
335
|
+
if (encoding) headers['Content-Encoding'] = encoding
|
|
336
|
+
headers['Content-Length'] = out.length
|
|
337
|
+
res.writeHead(status, headers)
|
|
338
|
+
res.end(out)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 请求方的 Accept-Encoding(`res.req` 由 Node 的 http server 挂上;取不到当不支持)。 */
|
|
342
|
+
export function acceptEncoding(res) {
|
|
343
|
+
try {
|
|
344
|
+
const req = res && (res.req || res.__meReq)
|
|
345
|
+
return String((req && req.headers && req.headers['accept-encoding']) || '').toLowerCase()
|
|
346
|
+
} catch { return '' }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 请求方是否接受 gzip。 */
|
|
350
|
+
export function acceptsGzip(res) {
|
|
351
|
+
return acceptEncoding(res).includes('gzip')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 协商压缩大响应体:
|
|
356
|
+
* - 支持 brotli 就优先 br(质量 4 = 在线压缩的速度/体积平衡点,比 gzip 再小约 30%)
|
|
357
|
+
* - 否则退 gzip
|
|
358
|
+
* - 都不支持、体积太小、或压完反而更大 → 明文
|
|
359
|
+
*/
|
|
360
|
+
export function encodeBody(res, buf) {
|
|
361
|
+
if (buf.length <= 2048) return { body: buf, encoding: '' }
|
|
362
|
+
const ae = acceptEncoding(res)
|
|
363
|
+
const wantBr = /(^|[,\s])br($|[,\s;])/.test(ae)
|
|
364
|
+
const wantGzip = ae.includes('gzip')
|
|
365
|
+
// 大响应体同时压 br + gzip 取更小的那个:br q5 在图谱这种高重复 JSON 上比 gzip 小 35%,
|
|
366
|
+
// 但在高熵的 JS/小 JSON 上偶尔略大 —— 取小者保证永不劣化(成本仅几毫秒)。
|
|
367
|
+
if (buf.length >= 16 * 1024) {
|
|
368
|
+
let br = null
|
|
369
|
+
if (wantBr) {
|
|
370
|
+
try { br = brotliCompressSync(buf, { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 5, [zlibConstants.BROTLI_PARAM_SIZE_HINT]: buf.length } }) } catch { br = null }
|
|
371
|
+
}
|
|
372
|
+
let gz = null
|
|
373
|
+
if (wantGzip) { try { gz = gzipSync(buf) } catch { gz = null } }
|
|
374
|
+
const best = [br && { body: br, encoding: 'br' }, gz && { body: gz, encoding: 'gzip' }]
|
|
375
|
+
.filter(Boolean)
|
|
376
|
+
.filter((x) => x.body.length < buf.length)
|
|
377
|
+
.sort((a, b) => a.body.length - b.body.length)[0]
|
|
378
|
+
if (best) return best
|
|
379
|
+
return { body: buf, encoding: '' }
|
|
380
|
+
}
|
|
381
|
+
if (wantBr) {
|
|
382
|
+
try {
|
|
383
|
+
const out = brotliCompressSync(buf, { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 5, [zlibConstants.BROTLI_PARAM_SIZE_HINT]: buf.length } })
|
|
384
|
+
if (out.length < buf.length) return { body: out, encoding: 'br' }
|
|
385
|
+
} catch { /* 落到 gzip */ }
|
|
386
|
+
}
|
|
387
|
+
if (wantGzip) {
|
|
388
|
+
const out = gzipSync(buf)
|
|
389
|
+
if (out.length < buf.length) return { body: out, encoding: 'gzip' }
|
|
390
|
+
}
|
|
391
|
+
return { body: buf, encoding: '' }
|
|
326
392
|
}
|
|
327
393
|
|
|
328
394
|
/** 读请求体(POST body),返回解析后的 JSON 对象或空。 */
|