memory-eternal 0.10.6 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/watchdog.js CHANGED
@@ -421,6 +421,203 @@ export async function waitForServedVersion(port, expect, { timeoutMs = 15000, in
421
421
  }
422
422
  }
423
423
 
424
+ // -- 常驻实例的「版本漂移自愈」--------------------------------------------------
425
+ //
426
+ // 为什么需要它:常驻 web(默认 7999)**比 DSH 宿主活得久**。升级 npm 包只换磁盘上的
427
+ // 文件,端口上那个进程仍在跑「启动时加载进内存」的旧代码;而 index.js 的启动策略是
428
+ // 「同端口已有活着的 watchdog 就让位(delegated)」,于是重启 DSH 多少次都不会替换它 ——
429
+ // 表现就是「启动 dsh 后,运行中依旧是旧版本」(issue #19 / #23)。
430
+ //
431
+ // 自愈的正确形态是「用新代码重启那个常驻进程」,**不是**热替换内存里的模块:ESM 模块缓存 +
432
+ // 闭包状态(settings / vault 路由 / SSE hub / fs.watch / 已监听的端口)没法安全热换,
433
+ // 缓存失效重 import 只会得到第二份模块图(双实例、双监听、双写)。
434
+ //
435
+ // 由此定下接口:
436
+ // inspectResident() 体检:端口上真正服务的版本 / 占用者 / 常驻看门狗
437
+ // decideResidentAction() 纯函数决策:delegate | spawn | restart | warn
438
+ // restartResident() 执行替换:spawn 一个 --replace 助手,再自检新版本
439
+ // clearResidentForTakeover() --replace 助手的主体:收掉旧实例 → 等端口空出来
440
+
441
+ /**
442
+ * 常驻实例体检(宿主启动自愈与 /restart-self 共用)。
443
+ *
444
+ * 版本一律**问端口**而不是读锁:锁里的 pkgVersion 是 watchdog 自己写的自述,
445
+ * 与端口上真正跑着的代码无关(issue #23)。
446
+ *
447
+ * @param {{env?:object, port:number, probeVersion?:Function, findListener?:Function}} opts
448
+ * @returns {Promise<{port:number, served:string, occupant:{pid:number,command:string}|null,
449
+ * occupantIsOurs:boolean, watchdog:object|null, watchdogAlive:boolean, residentStartedAt:number}>}
450
+ */
451
+ export async function inspectResident({ env = process.env, port, probeVersion, findListener } = {}) {
452
+ const p = Number(port) || 0
453
+ const entry = readWatchdogLock(env).watchdogs.find((w) => Number(w.port) === p && isPidAlive(w.pid)) || null
454
+ let served = ''
455
+ if (p) {
456
+ try { served = String((await (probeVersion || probeServedVersion)(p)) || '') } catch { served = '' }
457
+ }
458
+ let occupant = null
459
+ // 只有「探不到版本」时才需要问端口占用者:能自报版本就已经证明是我们的服务了
460
+ if (p && !served) {
461
+ try { occupant = await (findListener || findPortListener)(p) } catch { occupant = null }
462
+ }
463
+ const occupantIsOurs = Boolean(occupant && occupant.pid && looksLikeOurWeb(occupant.command))
464
+ return {
465
+ port: p,
466
+ served,
467
+ occupant,
468
+ occupantIsOurs,
469
+ watchdog: entry,
470
+ watchdogAlive: Boolean(entry),
471
+ residentStartedAt: entry ? (Date.parse(String(entry.startedAt || '')) || 0) : 0,
472
+ }
473
+ }
474
+
475
+ /**
476
+ * 「端口上的常驻实例该拿它怎么办」的纯决策(便于单测,不含任何 IO)。
477
+ *
478
+ * @param {object} o
479
+ * @param {string} [o.served] 端口上**真正服务**的版本(取不到 = 空串)
480
+ * @param {string} [o.expect] 本机磁盘上的包版本
481
+ * @param {boolean} [o.watchdogAlive] 同端口是否有活着的常驻 watchdog
482
+ * @param {boolean} [o.occupantIsOurs] 占用端口的进程是不是本插件的 web
483
+ * @param {boolean} [o.autoRestart] autoRestartOnDrift 开关
484
+ * @param {number} [o.residentStartedAt] 现有 watchdog 的启动时间(做防抖)
485
+ * @param {number} [o.now]
486
+ * @param {number} [o.debounceMs]
487
+ * @returns {{action:'delegate'|'spawn'|'restart'|'warn', stale:boolean, reason:string}}
488
+ */
489
+ export function decideResidentAction({
490
+ served = '', expect = '', watchdogAlive = false, occupantIsOurs = false,
491
+ autoRestart = true, residentStartedAt = 0, now = Date.now(), debounceMs = 60000,
492
+ } = {}) {
493
+ const s = String(served || '')
494
+ const e = String(expect || '')
495
+ // ① 版本一致:端口上跑的就是本机这份代码
496
+ if (s && e && s === e) return { action: 'delegate', stale: false, reason: 'up-to-date' }
497
+ // ② 读不到本机版本 → 没有判据,绝不乱动常驻实例(宁可不换,也不误杀)
498
+ if (!e) {
499
+ if (s) return { action: 'delegate', stale: false, reason: 'expect-unknown' }
500
+ return { action: watchdogAlive ? 'delegate' : 'spawn', stale: false, reason: 'expect-unknown' }
501
+ }
502
+ // ③ 漂移:自报版本不同,或干脆不自报版本(< v0.10.4 的旧 web)却占着端口
503
+ const drift = s ? s !== e : Boolean(occupantIsOurs)
504
+ if (!drift) {
505
+ if (s) return { action: 'delegate', stale: false, reason: 'up-to-date' }
506
+ return { action: watchdogAlive ? 'delegate' : 'spawn', stale: false, reason: 'port-idle' }
507
+ }
508
+ if (!autoRestart) return { action: 'warn', stale: true, reason: 'auto-restart-off' }
509
+ // ④ 防抖:刚替换过(新 watchdog 才起来)就别在效果重跑时反复重启
510
+ if (residentStartedAt && now - residentStartedAt < Math.max(0, Number(debounceMs) || 0)) {
511
+ return { action: 'delegate', stale: true, reason: 'debounced' }
512
+ }
513
+ return { action: 'restart', stale: true, reason: s ? 'version-drift' : 'resident-silent-version' }
514
+ }
515
+
516
+ /**
517
+ * spawn 一个 detached 的 `watchdog.js --replace` 助手。
518
+ *
519
+ * 为什么替换要交给子进程:调用方很可能**就是**端口上那个旧 web(/restart-self 由独立页发出),
520
+ * 就地收掉自己会让后面的「抢锁 + 拉起新 web」永远执行不到。助手不在锁里、也不占端口,
521
+ * 因此不会被自己收掉。失败不抛:调用方的版本自检会把「没换成功」如实报出来。
522
+ */
523
+ export function spawnReplaceHelper({ port = 7999, vaultRoot, interval = 5000, maxRestart = 10, spawnFn, bin } = {}) {
524
+ const script = path.join(__dirname, 'watchdog.js')
525
+ const args = [script, '--replace', '--port', String(port), '--interval', String(interval), '--max-restart', String(maxRestart)]
526
+ if (vaultRoot) args.push('--vault', vaultRoot)
527
+ const child = (spawnFn || spawn)(bin || nodeBinary(), args, {
528
+ detached: true,
529
+ stdio: 'ignore',
530
+ env: childEnv({ MEMORY_VAULT_DIR: vaultRoot || process.env.MEMORY_VAULT_DIR || '' }),
531
+ windowsHide: true,
532
+ })
533
+ if (child && typeof child.on === 'function') child.on('error', () => { /* 由调用方的自检暴露 */ })
534
+ if (child && typeof child.unref === 'function') child.unref()
535
+ return child
536
+ }
537
+
538
+ /**
539
+ * 用新代码重启常驻实例(= 「更新运行中的程序」唯一安全形态)。
540
+ *
541
+ * 步骤:spawn `--replace` 助手 → 助手收掉旧 watchdog / 旧 web、等端口空出来、抢锁、拉起新 web
542
+ * → 本进程轮询自检「端口上真正服务的版本 == 本机版本」(issue #23 建议 2)。
543
+ * 调用方若正是被替换的那个进程,会先被助手收掉(响应已发出),自检结论由新实例给出。
544
+ *
545
+ * @returns {Promise<{ok:boolean, stage:string, expect:string, served:string, waitedMs:number,
546
+ * helperPid:number, before:Array<object>, error?:string}>}
547
+ */
548
+ export async function restartResident({
549
+ port = 7999, vaultRoot, interval = 5000, maxRestart = 10, env = process.env,
550
+ expectVersion, timeoutMs = 25000, deps = {},
551
+ } = {}) {
552
+ const p = Number(port) || 7999
553
+ const expect = String(expectVersion !== undefined ? expectVersion : currentPkgVersion())
554
+ const before = readWatchdogLock(env).watchdogs.filter((w) => Number(w.port) === p)
555
+ let helperPid = 0
556
+ try {
557
+ const child = (deps.spawnHelper || spawnReplaceHelper)({ port: p, vaultRoot, interval, maxRestart })
558
+ helperPid = Number(child && child.pid) || 0
559
+ } catch (error) {
560
+ return { ok: false, stage: 'spawn-helper', expect, served: '', waitedMs: 0, helperPid: 0, before, error: String(error?.message || error) }
561
+ }
562
+ let check = { ok: false, version: '', waitedMs: 0 }
563
+ try { check = await (deps.waitForServedVersion || waitForServedVersion)(p, expect, { timeoutMs }) } catch { check = { ok: false, version: '', waitedMs: 0 } }
564
+ return {
565
+ ok: Boolean(check.ok),
566
+ stage: check.ok ? 'done' : 'verify',
567
+ expect,
568
+ served: String(check.version || ''),
569
+ waitedMs: Number(check.waitedMs) || 0,
570
+ helperPid,
571
+ before,
572
+ error: check.ok ? undefined : `端口 ${p} 上服务的版本是 ${check.version || '(无响应)'},本机是 ${expect || '?'}`,
573
+ }
574
+ }
575
+
576
+ /**
577
+ * `--replace` 助手的主体:收掉同端口的旧常驻实例 → 等端口真的空出来。
578
+ *
579
+ * 本进程既不在锁里、也不占端口,所以不会被自己收掉。
580
+ * portFree=false 时调用方必须**中止接管**:端口没空出来,新 web 只能退让到 port+1,
581
+ * 旧代码继续在目标端口上服务 —— 那正是「升级静默不生效」和「孤儿 web」的同一处病根。
582
+ */
583
+ export async function clearResidentForTakeover({ port = 7999, env = process.env, timeoutMs = 8000, deps = {} } = {}) {
584
+ const p = Number(port) || 7999
585
+ const findListener = deps.findListener || findPortListener
586
+ const out = { stopped: [], webStopped: [], portStopped: [], warnings: [], failed: [], portFree: false }
587
+ const rawKill = deps.kill || ((pid) => { try { process.kill(pid, 'SIGTERM'); return true } catch { return false } })
588
+ // 记下「已经判定收不掉」的 pid:立刻再来一次只会同样失败,白折腾还可能误导用户以为是权限抖动
589
+ const failedKills = new Set()
590
+ const killFn = (pid) => { const ok = rawKill(pid); if (!ok) failedKills.add(Number(pid)); return ok }
591
+ try {
592
+ const r = await (deps.stop || stopWatchdogs)({
593
+ env, port: p, checkPort: true, forcePortOccupant: true,
594
+ // 探测与杀戮都走同一个注入口:单测里既不能真去问生产端口,更不能真杀生产进程
595
+ kill: killFn, isAlive: deps.isAlive, sleep: deps.sleep, listProcesses: deps.listProcesses, findListener,
596
+ })
597
+ out.stopped = r.stopped || []
598
+ out.webStopped = r.webStopped || []
599
+ out.portStopped = r.portStopped || []
600
+ out.warnings = (r.warnings || []).slice()
601
+ out.failed = r.failed || []
602
+ } catch (error) {
603
+ out.warnings.push('停止旧实例失败:' + String(error?.message || error))
604
+ }
605
+ const wait = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
606
+ let left = null
607
+ try { left = await findListener(p) } catch { left = null }
608
+ const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0)
609
+ // 只对「确认是本插件的 web」下手:别人的进程占着端口时只如实报告,不误杀(同 issue #23 的口径)。
610
+ // 已经判定收不掉的 pid 不再重试 —— 立刻再来一次只会同样失败。
611
+ while (left && left.pid && looksLikeOurWeb(left.command) && !failedKills.has(Number(left.pid)) && Date.now() < deadline) {
612
+ if (!killFn(left.pid)) break
613
+ await wait(200)
614
+ try { left = await findListener(p) } catch { left = null }
615
+ }
616
+ out.portFree = !(left && left.pid)
617
+ if (!out.portFree) out.warnings.push(`端口 ${p} 仍被 pid ${left.pid} 占用(${left.command || '命令行未知'})`)
618
+ return out
619
+ }
620
+
424
621
  /**
425
622
  * 解析 `lsof -Fpcn` 的输出(每个进程一段:p<pid> / c<command> / n<name>)。
426
623
  * 纯函数,便于单测(POSIX 侧探测端口占用者用它)。
@@ -746,7 +943,18 @@ export function startWatchdog({ port = 7999, vaultRoot, interval = 5000, maxRest
746
943
  return { stop, get port() { return port }, get restartCount() { return restartCount } }
747
944
  }
748
945
 
749
- // 脚本入口:node lib/watchdog.js [--port N] [--interval MS] [--max-restart N] [--vault DIR]
946
+ /**
947
+ * 助手是被 detached + stdio ignore 拉起的,stderr 没人看 —— 关键结论必须落进 capture 日志,
948
+ * 否则「重启了却没生效」又会变成一条无迹可查的静默失败。
949
+ */
950
+ async function noteReplace(action, reason, env = process.env) {
951
+ try {
952
+ const { appendCaptureLog } = await import('./capture-log.js')
953
+ await appendCaptureLog({ time: Date.now(), sessionId: 'system', action, reason }, env)
954
+ } catch { /* 日志失败不影响接管 */ }
955
+ }
956
+
957
+ // 脚本入口:node lib/watchdog.js [--port N] [--interval MS] [--max-restart N] [--vault DIR] [--replace]
750
958
  if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
751
959
  const argv = process.argv.slice(2)
752
960
  const argOf = (name) => {
@@ -758,22 +966,44 @@ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
758
966
  const maxRestart = Number(argOf('--max-restart')) || 10
759
967
  const vaultRoot = argOf('--vault') || undefined
760
968
  const autoStart = !argv.includes('--no-restart')
969
+
970
+ /** 正常启动路径:抢锁 → 成为看门狗(--replace 接管成功后也走这里)。 */
971
+ const bootWatchdog = () => {
972
+ const lock = acquireWatchdogLock({ port, vault: vaultRoot || '', pkgVersion: currentPkgVersion() })
973
+ if (!lock.acquired) {
974
+ log('同端口已有看门狗在跑(pid ' + (lock.previous && lock.previous.pid) + ',启动于 ' + (lock.previous && lock.previous.startedAt) + ')→ 本次启动退出,避免进程堆积')
975
+ process.exitCode = 0
976
+ return
977
+ }
978
+ if (lock.previous) log('清理陈旧看门狗记录(pid ' + lock.previous.pid + ' 已不存在)')
979
+ process.on('exit', () => releaseWatchdogLock())
980
+ startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
981
+ log('看门狗已启动(pid ' + process.pid + ',端口 ' + port + ')')
982
+ }
983
+
761
984
  if (argv.includes('--reap')) {
762
985
  const out = await reapStaleWatchdogs({ port, keepPid: 0 })
763
986
  const msg = `回收孤儿看门狗:扫描 ${out.scanned} 个,清理 ${out.killed.length} 个${out.killed.length ? '(pid ' + out.killed.join(', ') + ')' : ''},保留 ${out.skipped.length} 个`
764
987
  // 必须同步写:process.exit() 会截断 stderr 的异步写入(重定向/管道时表现为「完全没有输出」)
765
988
  try { fs.writeSync(2, `[watchdog] ${msg}\n`) } catch { log(msg) }
766
989
  process.exitCode = 0
767
- } else {
768
- const lock = acquireWatchdogLock({ port, vault: vaultRoot || '' })
769
- if (!lock.acquired) {
770
- log('同端口已有看门狗在跑(pid ' + (lock.previous && lock.previous.pid) + ',启动于 ' + (lock.previous && lock.previous.startedAt) + ')→ 本次启动退出,避免进程堆积')
771
- process.exitCode = 0
772
- } else {
773
- if (lock.previous) log('清理陈旧看门狗记录(pid ' + lock.previous.pid + ' 已不存在)')
774
- process.on('exit', () => releaseWatchdogLock())
775
- startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
776
- log('看门狗已启动(pid ' + process.pid + ',端口 ' + port + ')')
990
+ } else if (argv.includes('--replace')) {
991
+ // 版本漂移自愈:先收掉旧常驻实例,再作为新一代看门狗接管(见 clearResidentForTakeover 的说明)。
992
+ const cleared = await clearResidentForTakeover({ port })
993
+ for (const w of cleared.warnings) log('⚠ ' + w)
994
+ const summary = `替换常驻实例:停止旧看门狗 ${cleared.stopped.length} 个`
995
+ + `、旧 web ${cleared.webStopped.length + cleared.portStopped.length} 个`
996
+ + `(端口 ${port}${cleared.portFree ? ' 已空出' : ' 仍被占用'})`
997
+ log(summary)
998
+ if (cleared.failed.length) log('⚠ 未能停止:pid ' + cleared.failed.join(', '))
999
+ if (!cleared.portFree) {
1000
+ const why = `${summary} —— 端口没空出来,已中止接管(不退让到 port+1,避免又留一个孤儿 web)`
1001
+ await noteReplace('fail', why)
1002
+ process.exit(1)
777
1003
  }
1004
+ await noteReplace('boot', summary)
1005
+ bootWatchdog()
1006
+ } else {
1007
+ bootWatchdog()
778
1008
  }
779
1009
  }
package/lib/web-assets.js CHANGED
@@ -1,41 +1,41 @@
1
- // 记忆核心 · 插件自带前端资源(web/ 目录)的完整性判据。
2
- //
3
- // 为什么单独一个模块:**宿主启动自检**(index.js)与**独立 web 进程兜底**(lib/web.js)
4
- // 必须用同一份判据和同一句措辞,否则两边会给出不一样的说法。
5
- //
6
- // 背景(issue #14):有人从插件市场装出来的副本里只有 package.json / lib/,`web/` 没落地
7
- // (发布物本身是齐的:npm 0.10.0 tgz 与 tag v0.10.0 都含 web/index.html)。那时侧边栏
8
- // 「记忆」的 iframe 只会拿到一页原始的 `{"ok":false,"error":"ENOENT ... web/index.html"}`,
9
- // 用户和维护者都看不出「装坏了、该怎么修」。
10
-
11
- import { existsSync } from 'node:fs'
12
- import path from 'node:path'
13
-
14
- /** 前端界面必需的静态资源(相对包的 web/ 目录)。 */
15
- export const WEB_ASSET_FILES = ['index.html', 'app.js']
16
-
17
- /**
18
- * 列出 web/ 下缺失的资源文件名;空数组 = 齐全。
19
- * @param {string} packageRoot 插件包根目录(含 package.json 的那一层)
20
- * @param {string[]} files 需要检查的文件名
21
- */
22
- export function missingWebAssets(packageRoot, files = WEB_ASSET_FILES) {
23
- return files.filter((f) => !existsSync(path.join(packageRoot, 'web', f)))
24
- }
25
-
26
- /** 缺 app.js 是硬故障:界面彻底起不来(缺 index.html 还能用内置外壳兜底)。 */
27
- export function isHardMissing(missing) {
28
- return (missing || []).includes('app.js')
29
- }
30
-
31
- /**
32
- * 写进「自动沉淀日志」的那句话(宿主与独立服务共用,保证两处措辞一致)。
33
- * 顺带给出可照做的修法,避免用户再开一个 issue 问「怎么修」。
34
- */
35
- export function missingAssetsReason(missing, packageRoot) {
36
- const list = (missing || []).map((f) => `web/${f}`).join('、')
37
- const where = path.join(packageRoot, 'web')
38
- const hard = isHardMissing(missing)
39
- return `插件安装不完整:缺少 ${list}(${where})——${hard ? '侧边栏「记忆」界面无法加载' : '已用内置外壳兜底,界面仍可用'}。`
40
- + `修法:完全退出 DSH → 删掉 ${path.join(path.dirname(packageRoot), 'memory-eternal')} → 重开 DSH 后「插件 → 添加插件」重新安装`
41
- }
1
+ // 记忆核心 · 插件自带前端资源(web/ 目录)的完整性判据。
2
+ //
3
+ // 为什么单独一个模块:**宿主启动自检**(index.js)与**独立 web 进程兜底**(lib/web.js)
4
+ // 必须用同一份判据和同一句措辞,否则两边会给出不一样的说法。
5
+ //
6
+ // 背景(issue #14):有人从插件市场装出来的副本里只有 package.json / lib/,`web/` 没落地
7
+ // (发布物本身是齐的:npm 0.10.0 tgz 与 tag v0.10.0 都含 web/index.html)。那时侧边栏
8
+ // 「记忆」的 iframe 只会拿到一页原始的 `{"ok":false,"error":"ENOENT ... web/index.html"}`,
9
+ // 用户和维护者都看不出「装坏了、该怎么修」。
10
+
11
+ import { existsSync } from 'node:fs'
12
+ import path from 'node:path'
13
+
14
+ /** 前端界面必需的静态资源(相对包的 web/ 目录)。 */
15
+ export const WEB_ASSET_FILES = ['index.html', 'app.js']
16
+
17
+ /**
18
+ * 列出 web/ 下缺失的资源文件名;空数组 = 齐全。
19
+ * @param {string} packageRoot 插件包根目录(含 package.json 的那一层)
20
+ * @param {string[]} files 需要检查的文件名
21
+ */
22
+ export function missingWebAssets(packageRoot, files = WEB_ASSET_FILES) {
23
+ return files.filter((f) => !existsSync(path.join(packageRoot, 'web', f)))
24
+ }
25
+
26
+ /** 缺 app.js 是硬故障:界面彻底起不来(缺 index.html 还能用内置外壳兜底)。 */
27
+ export function isHardMissing(missing) {
28
+ return (missing || []).includes('app.js')
29
+ }
30
+
31
+ /**
32
+ * 写进「自动沉淀日志」的那句话(宿主与独立服务共用,保证两处措辞一致)。
33
+ * 顺带给出可照做的修法,避免用户再开一个 issue 问「怎么修」。
34
+ */
35
+ export function missingAssetsReason(missing, packageRoot) {
36
+ const list = (missing || []).map((f) => `web/${f}`).join('、')
37
+ const where = path.join(packageRoot, 'web')
38
+ const hard = isHardMissing(missing)
39
+ return `插件安装不完整:缺少 ${list}(${where})——${hard ? '侧边栏「记忆」界面无法加载' : '已用内置外壳兜底,界面仍可用'}。`
40
+ + `修法:完全退出 DSH → 删掉 ${path.join(path.dirname(packageRoot), 'memory-eternal')} → 重开 DSH 后「插件 → 添加插件」重新安装`
41
+ }
package/lib/web.js CHANGED
@@ -147,6 +147,8 @@ export function startWebServer({ port = DEFAULT_WEB_PORT, vaultRoot = defaultVau
147
147
  const handleApi = createApi({
148
148
  vaultDir: () => vaultRoot,
149
149
  vaultRoots: () => [{ name: '', root: vaultRoot }],
150
+ // 本进程监听的就是「常驻实例」本身:/version-check 与 /restart-self 都要钉在这个端口上
151
+ selfPort: port,
150
152
  // 读 DSH 写入的共享配置文件 → web 端与 DSH 设置同步(不同步修复)
151
153
  getSettings: () => { try { return JSON.parse(readFileSync(configFilePath(process.env), 'utf8')) } catch { return {} } },
152
154
  getDshInfo: () => ({ name: 'deepseek-harness', label: 'DeepSeek Harness(当前宿主)', installed: true, memoryRecallTool: true, autoCapture: true, autoRecall: true, vaultDir: vaultRoot, version: VERSION }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  "prepublishOnly": "node build.mjs",
65
65
  "release": "node scripts/release.mjs",
66
66
  "release:dry": "node scripts/release.mjs --dry-run",
67
- "test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs"
67
+ "test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs"
68
68
  },
69
69
  "dependencies": {
70
70
  "@deepseek-ai/schemastery": "^3.18.1"
@@ -1,14 +1,19 @@
1
- ---
2
- name: memory
3
- description: Recall relevant knowledge cards from the unified memory vault and capture durable findings. Use memory_recall before answering when prior context helps; call memory_capture when a session produces a durable insight.
4
- ---
5
-
6
- # Memory Eternal
7
-
8
- This agent has access to `memory_recall` (retrieve cards) and `memory_capture` (store a card) via the `memory` MCP server.
9
-
10
- ## When to recall
11
- Before answering when the user's question may depend on prior project decisions, past bugs, or preferences. Combine with `memory_stats` to gauge coverage.
12
-
13
- ## When to capture
14
- At the end of a session that produced a durable, reusable conclusion (a fix, a decision, an architecture rationale, a constraint). Capture concise findings rather than raw transcripts; the hook also auto-captures at session end into the same vault (pending → audit). Source attribution is automatic.
1
+ ---
2
+ name: memory
3
+ description: Recall relevant knowledge cards from the unified memory vault and capture durable findings. Use memory_recall before answering when prior context helps; call memory_capture when a session produces a durable insight.
4
+ ---
5
+
6
+ # Memory Eternal
7
+
8
+ This agent has access to `memory_recall` (retrieve cards) and `memory_capture` (store a card) via the `memory` MCP server.
9
+
10
+ ## When to recall
11
+ Before answering when the user's question may depend on prior project decisions, past bugs, or preferences. Combine with `memory_stats` to gauge coverage.
12
+
13
+ ## When you need a credential (API key / token)
14
+ Recall first: `memory_recall("密钥 目录")` (or the vault's local wording). The memory vault stores the **directory** — key names, where they live, and the local command that uses them — **never the values**.
15
+ Then use the value through that command (typically a loader that injects it into a child process environment, e.g. `... --run -- <command>`).
16
+ Do **not** `cat`/read credential files (`~/.dsh/.credentials.yaml`, `.env`, `*-token`): reading them puts the secret into your context, which is a leak. Never print a secret, never write one into a card, commit, or issue.
17
+
18
+ ## When to capture
19
+ At the end of a session that produced a durable, reusable conclusion (a fix, a decision, an architecture rationale, a constraint). Capture concise findings rather than raw transcripts; the hook also auto-captures at session end into the same vault (pending → audit). Source attribution is automatic.