memory-eternal 0.10.39 → 0.10.41
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 +5 -3
- package/README.md +5 -3
- package/bin/dsh-memory.mjs +7 -1
- package/lib/client.js +46 -15
- package/lib/watchdog.js +98 -3
- package/package.json +2 -2
- package/web/app.js +49 -18
package/lib/watchdog.js
CHANGED
|
@@ -351,6 +351,8 @@ export async function stopWatchdogs({ env = process.env, port, timeoutMs = 5000,
|
|
|
351
351
|
for (const p of ports) {
|
|
352
352
|
const found = await probeListener(p)
|
|
353
353
|
if (found && found.pid) portOwners.push({ port: p, pid: Number(found.pid), command: String(found.command || '') })
|
|
354
|
+
// 「未知」要留痕(issue #45 后续):静默等于把人引回「端口空闲」的老误判
|
|
355
|
+
else if (found && found.unknown) warnings.push(`端口 ${p} 的占用者无法判定(进程枚举超时/不可用)—— 未知,不代表空闲`)
|
|
354
356
|
}
|
|
355
357
|
if (portOwners.length && forcePortOccupant) {
|
|
356
358
|
for (const occ of portOwners) {
|
|
@@ -576,6 +578,46 @@ export async function probe(port, timeoutMs = 1000) {
|
|
|
576
578
|
*
|
|
577
579
|
* @returns {Promise<string>} 取不到返回空串
|
|
578
580
|
*/
|
|
581
|
+
/**
|
|
582
|
+
* 内部:向某端口发一条 GET 并解析 JSON。**每次都销毁 socket**(issue #45 后续第 4 条)。
|
|
583
|
+
* @returns {Promise<{status:number, data:any, error:string}>}
|
|
584
|
+
*/
|
|
585
|
+
function requestJson(port, route, timeoutMs = 1200) {
|
|
586
|
+
return new Promise((resolve) => {
|
|
587
|
+
const req = http.request({ host: '127.0.0.1', port, path: route, method: 'GET', timeout: timeoutMs }, (res) => {
|
|
588
|
+
const chunks = []
|
|
589
|
+
res.on('data', (c) => chunks.push(c))
|
|
590
|
+
res.on('end', () => {
|
|
591
|
+
try { resolve({ status: res.statusCode || 0, data: JSON.parse(Buffer.concat(chunks).toString('utf8')), error: '' }) }
|
|
592
|
+
catch { resolve({ status: res.statusCode || 0, data: null, error: 'BAD_JSON' }) }
|
|
593
|
+
})
|
|
594
|
+
res.on('error', (e) => { try { res.destroy() } catch {} resolve({ status: 0, data: null, error: 'RES_ERROR:' + String((e && e.code) || e) }) })
|
|
595
|
+
})
|
|
596
|
+
req.on('error', (e) => resolve({ status: 0, data: null, error: String((e && e.code) || e) }))
|
|
597
|
+
req.on('timeout', () => { try { req.destroy() } catch {} resolve({ status: 0, data: null, error: 'TIMEOUT' }) })
|
|
598
|
+
req.end()
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 「端口上的版本」+ **失败原因**(issue #45 后续第 3 条:`超时` 与 `没有服务` 必须能分开报)。
|
|
604
|
+
* 纯诊断用;返回的 version 为空时,timedOut 告诉你这是超时还是根本没人应答。
|
|
605
|
+
* @returns {Promise<{version:string, timedOut:boolean, lastError:string}>}
|
|
606
|
+
*/
|
|
607
|
+
export async function probeServedVersionDetailed(port, timeoutMs = 1200, { tries = 3 } = {}) {
|
|
608
|
+
let lastError = ''
|
|
609
|
+
for (let i = 0; i < Math.max(1, tries); i++) {
|
|
610
|
+
const a = await requestJson(port, `${API_PREFIX}/version-check`, timeoutMs)
|
|
611
|
+
if (a.data && a.data.ok === true && typeof a.data.loaded === 'string' && a.data.loaded) return { version: String(a.data.loaded), timedOut: false, lastError: '' }
|
|
612
|
+
if (a.error) lastError = a.error
|
|
613
|
+
const b = await requestJson(port, `${API_PREFIX}/overview`, timeoutMs)
|
|
614
|
+
if (b.data && typeof b.data.version === 'string' && b.data.version) return { version: String(b.data.version), timedOut: false, lastError: '' }
|
|
615
|
+
if (b.error) lastError = b.error
|
|
616
|
+
if (i < tries - 1) await new Promise((r) => setTimeout(r, 150 * (i + 1)))
|
|
617
|
+
}
|
|
618
|
+
return { version: '', timedOut: /TIMEOUT/i.test(lastError), lastError }
|
|
619
|
+
}
|
|
620
|
+
|
|
579
621
|
export async function probeServedVersion(port, timeoutMs = 1200) {
|
|
580
622
|
// issue #31/#33 第②层:`/overview` 会碰数据库,库只读/损坏时它 500 —— 于是「端口上明明活着、
|
|
581
623
|
// 版本却拿不到」:verify 报「(无响应)」、status 甚至误判成「v0.10.4 之前的旧版」,进而触发
|
|
@@ -603,7 +645,7 @@ export async function probeServedVersion(port, timeoutMs = 1200) {
|
|
|
603
645
|
async function probeViaVersionCheck(port, timeoutMs = 1200) {
|
|
604
646
|
return new Promise((resolve) => {
|
|
605
647
|
const req = http.request({ host: '127.0.0.1', port, path: `${API_PREFIX}/version-check`, method: 'GET', timeout: timeoutMs }, (res) => {
|
|
606
|
-
if (res.statusCode !== 200) return resolve('')
|
|
648
|
+
if (res.statusCode !== 200) { try { res.resume(); res.destroy() } catch { /* 已关闭 */ } return resolve('') }
|
|
607
649
|
const chunks = []
|
|
608
650
|
res.on('data', (c) => chunks.push(c))
|
|
609
651
|
res.on('end', () => {
|
|
@@ -917,7 +959,8 @@ export async function clearResidentForTakeover({ port = 7999, env = process.env,
|
|
|
917
959
|
const findListener = deps.findListener || findPortListener
|
|
918
960
|
// 枚举本身要能被时间预算约束(否则 8s 预算被一次 13.6s 的 PowerShell 查询吃光,决策基于过期数据)
|
|
919
961
|
const listenerOpts = { timeoutMs: Math.max(500, Math.min(2500, Number(timeoutMs) || 8000)) }
|
|
920
|
-
|
|
962
|
+
// portFree 只是兼容布尔;**判定看 portState**(issue #45 后续:超时 ≠ 空闲)。
|
|
963
|
+
const out = { stopped: [], webStopped: [], portStopped: [], warnings: [], failed: [], portFree: false, portState: 'unknown' }
|
|
921
964
|
const rawKill = deps.kill || ((pid) => { try { process.kill(pid, 'SIGTERM'); return true } catch { return false } })
|
|
922
965
|
// 记下「已经判定收不掉」的 pid:立刻再来一次只会同样失败,白折腾还可能误导用户以为是权限抖动
|
|
923
966
|
const failedKills = new Set()
|
|
@@ -949,7 +992,14 @@ export async function clearResidentForTakeover({ port = 7999, env = process.env,
|
|
|
949
992
|
}
|
|
950
993
|
if (left && left.pid) {
|
|
951
994
|
out.portFree = false
|
|
995
|
+
out.portState = 'occupied'
|
|
952
996
|
out.warnings.push(`端口 ${p} 仍被 pid ${left.pid} 占用(${left.command || '命令行未知'})`)
|
|
997
|
+
} else if (left && left.unknown) {
|
|
998
|
+
// 枚举器超时/不可用 —— 这是「未知」,不是「空闲」:中止接管(不杀、不漂移、不动锁)。
|
|
999
|
+
out.portFree = false
|
|
1000
|
+
out.portState = 'unknown'
|
|
1001
|
+
out.unknown = true
|
|
1002
|
+
out.warnings.push(`端口 ${p} 的占用者**无法判定**(进程枚举超时/不可用)→ 已中止接管:既不按「端口空闲」继续,也不把 web 挪到 port+1`)
|
|
953
1003
|
} else {
|
|
954
1004
|
// 枚举不到 ≠ 端口是空的(issue #30 现场):受限环境/被拒的进程查询会让 findListener 返回 null,
|
|
955
1005
|
// 旧实现据此认为"端口已空出" → 新 web 绑不上 7999 → 退让或失败 → 自检「(无响应)」+ 锁被清空。
|
|
@@ -959,9 +1009,11 @@ export async function clearResidentForTakeover({ port = 7999, env = process.env,
|
|
|
959
1009
|
try { listening = Boolean(await probe(p)) } catch { listening = false }
|
|
960
1010
|
if (listening) {
|
|
961
1011
|
out.portFree = false
|
|
1012
|
+
out.portState = 'occupied'
|
|
962
1013
|
out.warnings.push(`端口 ${p} 仍在监听,但占用者无法识别(进程枚举被拒/不可用)—— 已中止接管,且未改动任何锁登记`)
|
|
963
1014
|
} else {
|
|
964
1015
|
out.portFree = true
|
|
1016
|
+
out.portState = 'free'
|
|
965
1017
|
}
|
|
966
1018
|
}
|
|
967
1019
|
return out
|
|
@@ -1001,6 +1053,31 @@ export function parseLsofFields(text) {
|
|
|
1001
1053
|
* @param {{exec?: (cmd:string,args:string[])=>Promise<string>}} [opts]
|
|
1002
1054
|
* @returns {Promise<{pid:number, command:string}|null>}
|
|
1003
1055
|
*/
|
|
1056
|
+
/**
|
|
1057
|
+
* 纯函数:从 `netstat -ano` 输出里找出**监听指定端口**的 pid(issue #45 后续实测)。
|
|
1058
|
+
*
|
|
1059
|
+
* 为什么它是 Windows 上的首选:报告者实测 `Get-NetTCPConnection` 在本机要 **4.1–61.4 秒**
|
|
1060
|
+
* (TCP 表本身很大,它要先枚举整表再过滤),而 `netstat -ano` 全量只要 **1.8 秒**,且直接给 pid。
|
|
1061
|
+
* netstat 不连端口、不枚举两次 —— 对「端口上那个进程把 backlog 打满、连不上」的现场
|
|
1062
|
+
* (正是 0.10.36 自探递归留下的状态)也能给出正确结论。
|
|
1063
|
+
*
|
|
1064
|
+
* @returns {number} pid,没找到返回 0
|
|
1065
|
+
*/
|
|
1066
|
+
export function parseNetstatListener(text, port) {
|
|
1067
|
+
const want = String(Number(port) || 0)
|
|
1068
|
+
if (!want || want === '0') return 0
|
|
1069
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
1070
|
+
if (!/LISTENING/i.test(line)) continue
|
|
1071
|
+
// 形如: TCP 127.0.0.1:7999 0.0.0.0:0 LISTENING 10924
|
|
1072
|
+
// TCP [::1]:7999 [::]:0 LISTENING 10924
|
|
1073
|
+
const m = /^\s*TCP\s+\S*:(\d+)\s+\S*:\d+\s+LISTENING\s+(\d+)\s*$/i.exec(line.trim())
|
|
1074
|
+
if (!m) continue
|
|
1075
|
+
if (String(Number(m[1])) !== want) continue
|
|
1076
|
+
return Number(m[2]) || 0
|
|
1077
|
+
}
|
|
1078
|
+
return 0
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1004
1081
|
export async function findPortListener(port, { exec, platform = process.platform, timeoutMs = 4000 } = {}) {
|
|
1005
1082
|
const p = Number(port)
|
|
1006
1083
|
if (!Number.isFinite(p) || p <= 0) return null
|
|
@@ -1008,6 +1085,24 @@ export async function findPortListener(port, { exec, platform = process.platform
|
|
|
1008
1085
|
const execOpts = { timeoutMs: Math.max(200, Number(timeoutMs) || 4000) }
|
|
1009
1086
|
try {
|
|
1010
1087
|
if (platform === 'win32') {
|
|
1088
|
+
// ① netstat -ano 优先(issue #45 后续):不连端口,1.8s 量级给出监听 pid。
|
|
1089
|
+
// PowerShell 的 Get-NetTCPConnection 在 TCP 表大的机器上实测 4.1–61.4s ——
|
|
1090
|
+
// 一旦超时,旧实现返回 null,调用方把它当「端口空闲」,于是新看门狗去绑一个**还被旧 web 占着**的端口,
|
|
1091
|
+
// 只能漂到 port+1,旧 web 永远活着、修复永远轮不到执行(报告者的现场)。
|
|
1092
|
+
let netOut = null
|
|
1093
|
+
try { netOut = await runExec('netstat', ['-ano'], { timeoutMs: Math.max(3000, execOpts.timeoutMs) }) } catch { netOut = null }
|
|
1094
|
+
const netPid = netOut == null ? 0 : parseNetstatListener(netOut, p)
|
|
1095
|
+
if (netPid) {
|
|
1096
|
+
let cmd = ''
|
|
1097
|
+
try {
|
|
1098
|
+
cmd = String((await runExec('powershell', ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${netPid}" | Select-Object -ExpandProperty CommandLine)`], execOpts)) || '').trim()
|
|
1099
|
+
} catch { cmd = '' }
|
|
1100
|
+
return { pid: netPid, command: cmd }
|
|
1101
|
+
}
|
|
1102
|
+
// netOut 为 null = 命令没跑起来/被杀超时 → **未知**,绝不能当成「端口空闲」
|
|
1103
|
+
// 空输出同样不可信:netstat 正常时至少会有一堆行;空 = 命令没跑起来/被超时杀掉(defaultExec 超时返回 '')
|
|
1104
|
+
if (netOut == null || String(netOut).trim() === '') return { pid: 0, command: '', unknown: true }
|
|
1105
|
+
// ② 兜底:老路径(netstat 明确没有该端口的 LISTEN 行时,用 PowerShell 再确认一次,兼容旧系统)
|
|
1011
1106
|
const out = await runExec('powershell', ['-NoProfile', '-Command', `Get-NetTCPConnection -LocalPort ${p} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`], execOpts)
|
|
1012
1107
|
const pid = Number(String(out || '').split(/\r?\n/).map((s) => s.trim()).find((s) => /^\d+$/.test(s))) || 0
|
|
1013
1108
|
if (!pid) return null
|
|
@@ -1400,7 +1495,7 @@ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
|
|
|
1400
1495
|
const claim = acquireWatchdogLock({ port, vault: vaultRoot || '', pkgVersion: currentPkgVersion(), force: true })
|
|
1401
1496
|
if (claim.acquired) log('已登记本次替换尝试(端口仍被占用,看门狗未启动):pid ' + process.pid)
|
|
1402
1497
|
else log('⚠ 未能登记本次替换(锁不可写?):' + (claim.file || ''))
|
|
1403
|
-
const why = `${summary} ——
|
|
1498
|
+
const why = `${summary} —— ${cleared.portState === 'unknown' ? '占用者无法判定(枚举超时/不可用),按未知处理' : '端口没空出来'},已中止接管(不退让到 port+1,避免又留一个孤儿 web)`
|
|
1404
1499
|
await noteReplace('fail', why)
|
|
1405
1500
|
process.exit(1)
|
|
1406
1501
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.41",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"prepublishOnly": "node build.mjs",
|
|
65
65
|
"release": "node scripts/release.mjs",
|
|
66
66
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
67
|
-
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs"
|
|
67
|
+
"test": "node build.mjs && node tests/vault.test.mjs && node tests/capture.test.mjs && node tests/capture-route.test.mjs && node tests/stall.test.mjs && node tests/vault-path.test.mjs && node tests/vault-resolve.test.mjs && node tests/minhash.test.mjs && node tests/settings-save.test.mjs && node tests/config-coverage.test.mjs && node tests/config-sync.test.mjs && node tests/config-merge.test.mjs && node tests/sse.test.mjs && node tests/api.test.mjs && node tests/import-roundtrip.test.mjs && node tests/graph-perf.test.mjs && node tests/settings-compat.test.mjs && node tests/node-bin.test.mjs && node tests/watchdog-lock.test.mjs && node tests/markdown.test.mjs && node tests/feedback.test.mjs && node tests/graph-lod.test.mjs && node tests/ssr.test.mjs && node tests/web-assets.test.mjs && node tests/manifests.test.mjs && node tests/two-store.test.mjs && node tests/noise-gate.test.mjs && node tests/e2e-recall.test.mjs && node tests/issue-fixes.test.mjs && node tests/issue-fixes-2.test.mjs && node tests/issue-fixes-3.test.mjs && node tests/watchdog-orphans.test.mjs && node tests/watchdog-restart.test.mjs && node tests/readme-i18n.test.mjs && node tests/issue-fixes-4.test.mjs && node tests/host-drain.test.mjs && node tests/e2e-host-drain.mjs && node tests/issue-fixes-5.test.mjs && node tests/issue-fixes-6.test.mjs && node tests/capture-json-repair.test.mjs && node tests/csrf-origin.test.mjs && node tests/watchdog-orphan-web.test.mjs && node tests/config-validate.test.mjs && node tests/issue-fixes-8.test.mjs && node tests/issue-fixes-9.test.mjs && node tests/release-verify.test.mjs && node tests/refine.test.mjs && node tests/db-restore.test.mjs && node tests/align.test.mjs && node tests/tags-normalize.test.mjs && node tests/resident-state.test.mjs && node tests/llm-route.test.mjs && node tests/host-managed.test.mjs && node tests/replace-claim.test.mjs && node tests/version-check-self.test.mjs && node tests/setup-codex.test.mjs && node tests/listener-netstat.test.mjs && node tests/issue-46-overlay.test.mjs && node tests/graph-initial-zoom.test.mjs"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|