🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

记忆守护监管器 Nesarf/volcano-separator

DeepSeek Harness
Nesarf31 天前有提交已提供中文文档扫描:含敏感能力

分阶段预热并守护记忆进程,避免启动超时

30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/1 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

自动兼容检查有告警:未发布到 npm registry,仅可从源码安装。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Nesarf/volcano-separator
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:待复验
是什么
dsh 原生插件 · tool
装得上吗
曾在 dsh 0.2.0-rc.2 下真实安装成功,但当前 dsh 已更新至 0.2.1-rc.5,本站尚未在新版下复验
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 31 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意

未发布到 npm registry,仅可从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包volcano-separator(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=20.12 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/8 09:41:36

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 188 处,展示前 139 条)
  • 读取本地文件
    lib/core.mjs:134
    for (const p of paths) if (p && existsSync(p)) return p
  • 读取本地文件
    lib/core.mjs:146
    if (statSync(p).isFile()) return p
  • 读取本地文件
    lib/core.mjs:187
    if (uvCacheDir && existsSync(uvCacheDir)) {
  • 读取本地文件
    lib/mcp.mjs:57
    const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
  • 读取本地文件
    lib/platform.mjs:38
    const raw = readFileSync(file, 'utf8')
  • 执行外部命令来自配置/变量
    lib/platform.mjs:100
    child = spawn(cmd, args, { env, cwd, windowsHide: true })
  • 写入本地文件
    lib/platform.mjs:115
    appendFileSync(logFile, chunk)
  • 写入本地文件
    lib/platform.mjs:124
    mkdirSync(join(logFile, '..'), { recursive: true })
  • 读取本地文件
    lib/activity.mjs:35
    list = readdirSync(dir)
  • 读取本地文件
    lib/activity.mjs:37
    .map((f) => ({ f, t: statSync(join(dir, f)).mtimeMs }))
  • 读取本地文件
    lib/activity.mjs:59
    text = readFileSync(join(dir, entry.f), 'utf8')
  • 读取本地文件
    lib/activity.mjs:155
    if (!existsSync(file)) {
  • 读取本地文件
    lib/activity.mjs:160
    raw = JSON.parse(readFileSync(file, 'utf8').replace(/^ FEFF/, ''))
  • 读取本地文件
    lib/activity.mjs:271
    if (!existsSync(watch)) return { ok: false, detail: `watcher not found: ${watch}` }
  • 写入本地文件
    lib/activity.mjs:302
    writeFileSync(vbs, script, 'utf8')
  • 写入本地文件
    lib/activity.mjs:407
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/activity.mjs:413
    appendFileSync(file, JSON.stringify(event) + '\n', 'utf8')
  • 读取本地文件
    lib/live.mjs:57
    files = readdirSync(dir)
  • 读取本地文件
    lib/live.mjs:59
    .map((f) => ({ f, t: statSync(join(dir, f)).mtimeMs, size: statSync(join(dir, f)).size }))
  • 读取本地文件
    lib/live.mjs:67
    text = readFileSync(join(dir, entry.f), 'utf8')
  • 读取本地文件
    lib/live.mjs:81
    if (existsSync(hb)) {
  • 读取本地文件
    lib/live.mjs:84
    text = readFileSync(hb, 'utf8')
  • 读取本地文件
    lib/live.mjs:105
    if (!existsSync(script)) return { ok: false, detail: `reveal script missing: ${script}` }
  • 读取本地文件
    lib/service.mjs:312
    if (configFile && existsSync(configFile)) {
  • 读取本地文件
    lib/redline.mjs:55
    if (!existsSync(area.dir)) continue
  • 读取本地文件
    lib/redline.mjs:72
    entries = readdirSync(dir, { withFileTypes: true })
  • 读取本地文件
    lib/redline.mjs:85
    size = statSync(full).size
  • 读取本地文件
    lib/redline.mjs:130
    for (const line of readFileSync(file, 'utf8').split(String.fromCharCode(10))) {
  • 写入本地文件
    lib/redline.mjs:147
    appendFileSync(file, JSON.stringify({
  • 读取本地文件
    lib/rules.mjs:109
    return readFileSync(RULES_DOC, 'utf8')
  • 读取本地文件
    lib/entity.mjs:194
    pathExists: exe ? existsSync(exe) : null,
  • 读取本地文件
    lib/entity.mjs:228
    snapshot.push({ surface: 'filesystem', what: 'the path itself', value: { exists: existsSync(path) } })
  • 读取本地文件
    lib/entity.mjs:363
    pathExists: exe ? existsSync(exe) : null,
  • 写入本地文件
    lib/supervisor.mjs:63
    mkdirSync(dir, { recursive: true })
  • 读取本地文件
    lib/supervisor.mjs:68
    const files = readdirSync(dir)
  • 读取本地文件
    lib/supervisor.mjs:70
    .map((f) => ({ f, t: statSync(join(dir, f)).mtimeMs }))
  • 删除本地文件
    lib/supervisor.mjs:74
    rmSync(join(dir, old.f), { force: true })
  • 读取本地文件
    lib/supervisor.mjs:96
    for (let n = 2; existsSync(file) && n < 100; n++) {
  • 读取本地文件
    lib/supervisor.mjs:130
    names = readdirSync(base)
  • 读取本地文件
    lib/supervisor.mjs:137
    if (existsSync(p)) out.push(p)
  • 发起网络请求
    lib/supervisor.mjs:210
    res = await fetch(`${base}${spec.path}`, { signal: ctl.signal }).catch(() => null)
  • 读取本地文件
    lib/supervisor.mjs:301
    return { dataDir: resolve(ctx.pgDataDir), deployment: 'declared', exists: existsSync(resolve(ctx.pgDataDir)), why: 'ctx.pgDataDir' }
  • 写入本地文件
    lib/supervisor.mjs:1148
    mkdirSync(dirname(logFile), { recursive: true })
  • 写入本地文件
    lib/supervisor.mjs:1149
    const log = (line) => appendFileSync(logFile, `${new Date().toISOString()} ${line}\r\n`)
  • 写入本地文件
    lib/supervisor.mjs:1394
    mkdirSync(join(projectDir, 'bin'), { recursive: true })
  • 写入本地文件
    lib/supervisor.mjs:1395
    writeFileSync(scriptPath, script, 'utf8')
  • 读取本地文件
    lib/signals.mjs:174
    if (exe && !systemRoot && !existsSync(exe)) {
  • 读取本地文件
    lib/signals.mjs:305
    for (const line of readFileSync(file, 'utf8').split(String.fromCharCode(10))) {
  • 写入本地文件
    lib/signals.mjs:344
    appendFileSync(file, line + String.fromCharCode(10))
  • 读取本地文件
    lib/signals.mjs:392
    existing = readFileSync(file, 'utf8')
  • 写入本地文件
    lib/signals.mjs:451
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/signals.mjs:452
    writeFileSync(file, kept.concat(line).join(String.fromCharCode(10)) + String.fromCharCode(10))
  • 读取本地文件
    lib/signals.mjs:615
    return readFileSync(file, 'utf8')
  • 读取本地文件
    lib/custody.mjs:60
    files = readdirSync(dir).filter((f) => /^activity-\d{4}-\d{2}-\d{2}\.ndjson$/.test(f))
  • 读取本地文件
    lib/custody.mjs:67
    text = readFileSync(join(dir, f), 'utf8')
  • 读取本地文件
    lib/custody.mjs:95
    files = readdirSync(dir).filter((f) => /^detain-\d+\.json$/.test(f))
  • 读取本地文件
    lib/custody.mjs:202
    files = readdirSync(dir).filter((f) => /^activity-\d{4}-\d{2}-\d{2}\.ndjson$/.test(f)).sort()
  • 读取本地文件
    lib/custody.mjs:212
    text = readFileSync(join(dir, f), 'utf8')
  • 读取本地文件
    lib/custody.mjs:281
    files = readdirSync(dir).filter((x) => x.startsWith('detain-') && x.endsWith('.json'))
  • 删除本地文件
    lib/custody.mjs:692
    rmSync(summaryFile, { force: true })
  • 读取本地文件
    lib/policy.mjs:66
    let integrity = existsSync(file) ? 'ok' : 'absent'
  • 读取本地文件
    lib/policy.mjs:70
    raw = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    lib/policy.mjs:99
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    lib/policy.mjs:105
    writeFileSync(tmp, JSON.stringify(policy, null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/policy.mjs:106
    renameSync(tmp, file)
  • 删除本地文件
    lib/policy.mjs:110
    rmSync(tmp, { force: true })
  • 读取本地文件
    lib/uvcache.mjs:54
    entries = readdirSync(d, { withFileTypes: true })
  • 读取本地文件
    lib/uvcache.mjs:64
    total += statSync(full).size
  • 读取本地文件
    lib/uvcache.mjs:99
    if (!root || !existsSync(root)) {
  • 读取本地文件
    lib/uvcache.mjs:103
    if (!existsSync(archiveDir)) {
  • 读取本地文件
    lib/uvcache.mjs:109
    for (const sub of readdirSync(root)) {
  • 读取本地文件
    lib/uvcache.mjs:112
    if (!statSync(p).isDirectory()) continue
  • 写入本地文件
    lib/uvcache.mjs:269
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/uvcache.mjs:272
    writeFileSync(tmp, JSON.stringify(plan, null, 2) + NL, 'utf8')
  • 写入本地文件
    lib/uvcache.mjs:274
    renameSync(tmp, file)
  • 删除本地文件
    lib/uvcache.mjs:279
    try { unlinkSync(tmp) } catch { }
  • 删除本地文件
    lib/uvcache.mjs:282
    try { unlinkSync(tmp) } catch { }
  • 删除本地文件
    lib/uvcache.mjs:290
    unlinkSync(old.file)
  • 删除本地文件递归
    lib/uvcache.mjs:392
    rmSync(trash, { recursive: true, force: true })
  • 写入本地文件
    lib/uvcache.mjs:396
    mkdirSync(trash, { recursive: true })
  • 写入本地文件
    lib/uvcache.mjs:432
    renameSync(t.dir, staged)
  • 删除本地文件递归
    lib/uvcache.mjs:440
    rmSync(staged, { recursive: true, force: true })
  • 读取本地文件
    lib/enforce.mjs:61
    if (!existsSync(script)) return { ok: false, detail: `isolation script missing: ${script}` }
  • 读取本地文件
    lib/enforce.mjs:112
    if (!existsSync(dir)) return { ok: true, dir, entries: [] }
  • 读取本地文件
    lib/enforce.mjs:114
    for (const f of readdirSync(dir).filter((n) => n.endsWith('.json')).sort()) {
  • 读取本地文件
    lib/enforce.mjs:127
    backupExists: spec.backupFile ? existsSync(spec.backupFile) : false,
  • 读取本地文件
    lib/enforce.mjs:166
    if (!existsSync(script)) return {}
  • 写入本地文件
    lib/enforce.mjs:169
    writeFileSync(listFile, list.join(String.fromCharCode(10)))
  • 写入本地文件
    lib/crypt.mjs:100
    mkdirSync(dirname(file), { recursive: true })
  • 读取本地文件
    lib/crypt.mjs:104
    if (!existsSync(script)) return { ok: false, detail: `keyprotect script missing: ${script}` }
  • 读取本地文件
    lib/crypt.mjs:106
    if (existsSync(file)) {
  • 读取本地文件
    lib/crypt.mjs:111
    const raw = Buffer.from(readFileSync(plain, 'utf8').trim(), 'base64')
  • 删除本地文件
    lib/crypt.mjs:115
    rmSync(plain, { force: true })
  • 写入本地文件
    lib/crypt.mjs:121
    writeFileSync(plain, raw.toString('base64'), { encoding: 'ascii' })
  • 删除本地文件
    lib/crypt.mjs:126
    rmSync(plain, { force: true })
  • 读取本地文件
    lib/crypt.mjs:184
    return { ok: true, size: statSync(path).size }
  • 读取本地文件
    lib/crypt.mjs:203
    if (!existsSync(path)) return { ok: false, detail: `no such file: ${path}` }
  • 读取本地文件
    lib/crypt.mjs:207
    st = statSync(path)
  • 写入本地文件
    lib/crypt.mjs:239
    mkdirSync(jdir, { recursive: true })
  • 写入本地文件
    lib/crypt.mjs:262
    writeFileSync(journal, JSON.stringify(spec, null, 2))
  • 写入本地文件
    lib/crypt.mjs:268
    writeFileSync(tmp, blob)
  • 删除本地文件
    lib/crypt.mjs:270
    rmSync(tmp, { force: true })
  • 写入本地文件
    lib/crypt.mjs:278
    writeFileSync(stray, back)
  • 删除本地文件
    lib/crypt.mjs:283
    rmSync(stray, { force: true })
  • 删除本地文件
    lib/crypt.mjs:286
    rmSync(tmp, { force: true })
  • 删除本地文件
    lib/crypt.mjs:293
    rmSync(tmp, { force: true })
  • 写入本地文件
    lib/vault.mjs:80
    mkdirSync(dirname(file), { recursive: true })
  • 读取本地文件
    lib/vault.mjs:84
    if (!existsSync(script)) return { ok: false, detail: `keyprotect script missing: ${script}` }
  • 读取本地文件
    lib/vault.mjs:86
    if (existsSync(file)) {
  • 读取本地文件
    lib/vault.mjs:93
    if (!existsSync(plain)) return { ok: false, detail: r.error ?? 'unprotect produced no key' }
  • 读取本地文件
    lib/vault.mjs:94
    const raw = Buffer.from(readFileSync(plain, 'utf8').trim(), 'base64')
  • 删除本地文件
    lib/vault.mjs:98
    rmSync(plain, { force: true })
  • 写入本地文件
    lib/vault.mjs:104
    writeFileSync(plain, raw.toString('base64'), { encoding: 'ascii' })
  • 删除本地文件
    lib/vault.mjs:109
    rmSync(plain, { force: true })
  • 读取本地文件
    lib/vault.mjs:110
    if (!existsSync(file)) return { ok: false, detail: r.error ?? 'could not protect the new key' }
  • 读取本地文件
    lib/vault.mjs:139
    h.update(readFileSync(file))
  • 写入本地文件
    lib/vault.mjs:208
    mkdirSync(entryDir, { recursive: true })
  • 写入本地文件
    lib/vault.mjs:209
    copyFileSync(src, vaultPath)
  • 删除本地文件递归
    lib/vault.mjs:211
    rmSync(entryDir, { recursive: true, force: true })
  • 删除本地文件递归
    lib/vault.mjs:220
    rmSync(entryDir, { recursive: true, force: true })
  • 写入本地文件
    lib/vault.mjs:240
    writeFileSync(join(entryDir, MANIFEST), JSON.stringify(spec, null, 1) + String.fromCharCode(10), 'utf8')
  • 删除本地文件递归
    lib/vault.mjs:242
    rmSync(entryDir, { recursive: true, force: true })
  • 删除本地文件
    lib/vault.mjs:254
    rmSync(src, { force: true })
  • 写入本地文件
    lib/vault.mjs:297
    mkdirSync(dirname(file), { recursive: true })
  • 读取本地文件
    lib/chamber.mjs:49
    if (!existsSync(script)) return { ok: false, pid: target, detail: `process probe script missing: ${script}` }
  • 删除本地文件
    lib/chamber.mjs:54
    rmSync(outFile, { force: true })
  • 删除本地文件
    lib/chamber.mjs:65
    rmSync(outFile, { force: true })
  • 读取本地文件
    lib/chamber.mjs:169
    if (!existsSync(script)) return { ok: false, detail: `the chamber window script is missing: ${script}` }
  • 写入本地文件
    lib/chamber.mjs:178
    writeFileSync(profile, JSON.stringify(p, null, 1), 'utf8')
  • 读取本地文件
    lib/chamber.mjs:204
    return existsSync(chamberScript())
  • 读取本地文件
    lib/lock.mjs:45
    if (!existsSync(file)) return null
  • 写入本地文件
    lib/lock.mjs:97
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    lib/lock.mjs:100
    writeFileSync(file, JSON.stringify(me, null, 2))
  • 删除本地文件
    lib/lock.mjs:117
    rmSync(file, { force: true })
  • 读取本地文件
    lib/database.mjs:42
    if (!existsSync(root)) continue
  • 读取本地文件
    lib/database.mjs:44
    const versions = readdirSync(root).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
  • 读取本地文件
    lib/database.mjs:47
    if (existsSync(join(bin, 'psql.exe'))) return bin
  • 读取本地文件
    lib/database.mjs:65
    if (!existsSync(file)) return null
  • 读取本地文件
    lib/database.mjs:67
    for (const line of readFileSync(file, 'utf8').split(String.fromCharCode(10))) {
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 分阶段预热记忆进程
  • 心跳监控守护进程
  • 解耦脆弱启动链路

将 uv、Hindsight 守护进程及其数据从一条脆弱的链路中解耦:无看门狗预热,数秒内提供服务,按心跳监控。

README

由 DeepSeek 最新模型翻译生成
Volcano Separator CI 将三样被焊死在一起的东西分开。 volcano-separator 是 Hindsight 记忆守护进程的监管器。它修复的不是一个 bug——它修复的是一种结构性脆弱: uvx(包安装器) └─ uv 缓存中的一个临时环境 ← 守护进程自身的代码就在这里 └─ hindsight 守护进程 ← 由宿主插件按需启动, └─ Postgres (pg0) 仅在会话开始时 / 在 retain 时 ← 数据就在这里 一个包安装器的状态决定了服务能否启动,而服务的生命周期又由宿主的会话时序决定。当任何一环感冒时,记忆就会消失——悄无声息地。 它实际治疗的是什么 在提取出本工具的机器上测得(volcano-separator doctor): 12 次启动尝试,7 次因超时失败,1 次成功 每一条失败日志都在启动路径上显示 Downloaded botocore / Built claude-agent-sdk==0.2.16x。 一次构建和一个看门狗在互相竞速——插件的看门狗允许约 180 秒,而在机械硬盘上冷缓存的 uvx 运行需要的时间远不止于此。它每次都输。
展开完整文档(README 剩余 1 节)
Volcano Separator

CI

将三样被焊死在一起的东西分开。

volcano-separator 是 Hindsight 记忆守护进程的监管器。它修复的不是一个 bug——它修复的是一种结构性脆弱:

uvx(包安装器) └─ uv 缓存中的一个临时环境 ← 守护进程自身的代码就在这里 └─ hindsight 守护进程 ← 由宿主插件按需启动, └─ Postgres (pg0) 仅在会话开始时 / 在 retain 时 ← 数据就在这里

一个包安装器的状态决定了服务能否启动,而服务的生命周期又由宿主的会话时序决定。当任何一环感冒时,记忆就会消失——悄无声息地。

它实际治疗的是什么

在提取出本工具的机器上测得(volcano-separator doctor):

12 次启动尝试,7 次因超时失败,1 次成功

每一条失败日志都在启动路径上显示 Downloaded botocore / Built claude-agent-sdk==0.2.16x。
一次构建和一个看门狗在互相竞速——插件的看门狗允许约 180 秒,而在机械硬盘上冷缓存的 uvx 运行需要的时间远不止于此。它每次都输。

修复方案:将工作分阶段,使任何看门狗都不会与构建竞速

| 阶段 | 它做什么 | 看门狗 |
| warm | 确保 uv 环境完整(幂等) | 无——它可能需要数分钟;它会运行至完成 |
| serve | 仅在环境已热时启动守护进程 | 有,但此时只需数秒——它永远不会触发 |
| watch | 按需进行健康探测与修复 | 心跳,健康时成本近乎为零 |

让这一切奏效的特性是:heal 在健康时耗时约 0.9 秒(一次 TCP 探测)。因此它可以每隔几分钟被调用一次,而无需保留一个常驻的监管进程——调度即监视。

修复的另一半在本工具的上游:将 UV_CACHE_DIR 指向一块 SSD,并把旧路径做成 junction,这样即使是冷启动也很快。

安装

git clone https://github.com//volcano-separator.git
cd volcano-separator
node bin/cli.mjs status # 无需安装步骤:零依赖

可选地将其加入 PATH:

npm link # 提供 volcano-separator 和 vsep

用法

volcano-separator status # 全链路检查(uv / 环境热度 / 守护进程 / pg / 守护进程日志错误 / 看门狗) volcano-separator heal # 智能修复:warm -> serve -> watch volcano-separator warm [--force] # 仅预热环境(无看门狗) volcano-separator serve # 仅启动服务 volcano-separator stop | restart volcano-separator doctor # 从插件日志统计历史启动失败次数 volcano-separator guard clean # 检查此刻执行 uv 缓存操作是否安全 volcano-separator install-service # 注册看门狗任务(登录时 + 每 N 分钟) volcano-separator service # 看门狗任务状态

选项:--profile (默认 coding-agent)、--port (默认 9077)、--json、--quiet、--force、--dry-run。

三种接口

| 形式 | 入口点 | 适用对象 |
| CLI | bin/cli.mjs | 人类、脚本、计划任务 |
| MCP | lib/mcp.mjs | 任何 harness —— volcano_status / volcano_heal / volcano_doctor |
| 库 | lib/core.mjs | 其他工具(纯 Node,零依赖) |

MCP 客户端配置: json {
"mcpServers": {
"volcano-separator": { "command": "node", "args": ["/path/to/volcano-separator/lib/mcp.mjs"] }
}
}

它为你处理的三个真实陷阱

1. 守护进程的代码位于 uv 缓存内部。
因此 uv cache clean / prune 会被正在运行的守护进程的占用锁阻止,而 --force 会在守护进程运行时删除它自己的文件。更糟的是,当它还活着时,缓存目录甚至无法被重命名(访问被拒绝)。
volcano-separator guard 会先检查并打印安全顺序:stop -> uv cache -> heal。

2. 由宿主插件启动的守护进程不被 embed 管理器识别。
hindsight-embed daemon stop 跟踪自己的 PID;宿主插件自己启动的守护进程不在该记录中,因此:

WARNING - Could not find PID for port 9077
WARNING - Port 9077 is bound but no hindsight daemon could be identified on it
Failed to stop daemon

因此 stop 有两个层级:先走官方路径,然后找到端口占用者并收集其进程树(仅限 uv/uvx/python/hindsight 进程——绝不包括其他任何东西)。没有这一层,一个由插件启动的守护进程会让监督程序永远无法停止它。

3. “端口有响应”不等于“它能工作”。
守护进程可以让 /health 保持绿色,却无法连接到它的数据库。在实际环境中观察到:磁盘饱和(一次后台文件清理)导致守护进程的 Postgres 连接握手超时 48 次,端口从未闪烁,而每次记忆写入都毫无可见原因地停滞。这就是为什么 status 还会探测 Postgres 并扫描守护进程自己的日志以查找错误——也是为什么它使用两个时间窗口:最近 5 分钟内的错误意味着它现在正在失败,而更早的任何内容都作为上下文(recovered)报告,这样过去的事件就不会让链条永远被标记为不健康。

设计边界

- 它不修补宿主插件。 插件的 ensureDaemon() 以 if (await isServerHealthy(...)) return; 开头——一旦服务已经健康,它会立即返回,那条脆弱的路径永远不会被进入。那是“不再被遍历”,而不是“被修补”。
- 没有额外的常驻进程。 监视被委托给操作系统调度器。
- 它只读取日志;它从不触碰 hindsight 自己的数据。

移植一切皆靠发现,而非硬编码:uvx 来自 PATH,缓存目录来自 UV_CACHE_DIR(并将 junction 解析为其真实目标),嵌入版本来自宿主配置。

在 lib/core.mjs 顶部的 DEFAULTS 中调整行为: js profile: 'coding-agent', port: 9077,
withPackages: ['pg0-embedded'],
warmProbeMs: 15000, // 一次干跑慢于此值意味着环境是冷的 warmBudgetMs: 15 * 60 * 1000, serveBudgetMs: 5 * 60 * 1000, taskName: 'Volcano-Separator', taskIntervalMinutes: 5,

平台说明:计划任务集成目前仅限 Windows(schtasks / New-ScheduledTask)。暂存模型本身是可移植的——在 macOS/Linux 上,将 installService() 替换为 launchd plist 或 systemd timer。

要求

Node >= 20.12。零第三方依赖。

测试 bash
node test/smoke.mjs # 或:npm test

冒烟测试刻意保持离线:无网络、无 uv、无运行中的守护进程。它断言对监督器至关重要的属性——模块能加载,每个探针在其依赖缺失时降级而非抛出异常,缓存护栏在没有任何运行中的进程时报告“安全”,以及 CLI 的退出码行为正常。CI 在 Node 20 和 22 上运行它,覆盖 Linux 和 Windows。

许可证

MIT
预览分享卡片长什么样 ↓Nesarf/volcano-separator
分享 · DeepSeek Harness Hub
Nesarf/volcano-separator

分阶段预热并守护记忆进程,避免启动超时

回到顶部