dsh-hindsight-daemon
给 DSH Desktop 用的 Hindsight 本地记忆 daemon 生命周期插件。
DSH 启动时自动把 Hindsight daemon 拉起来,DSH 退出时把它连同它依赖的嵌入式 PostgreSQL 一起停掉 —— 不产生任何常驻进程,也不需要开机自启项、计划任务或后台监视器。 安装完成后在「设置 → Hindsight 记忆」里有一个独立页面,可以看状态、装依赖、启停服务、改配置。
支持 Windows 10/11 与 Linux(x86_64)。Windows 用 PowerShell 脚本安装, Linux 用
scripts/install.sh;两端的进程收尾分别落在taskkill与pg_ctl/pkill上。
为什么需要这个插件
Hindsight 的 coding-agent 集成有三种模式:cloud、self-hosted、daemon。
选 daemon 时,记忆存在本机(嵌入式 PostgreSQL + pg0),只有"抽取事实"这一步会调用
你自己的 LLM —— 数据不出机器,也不需要任何云端账号。
但官方插件在 daemon 模式下有两个限制,导致它无法自己把 daemon 拉起来:
- 它只认进程环境变量。
detectLlm()读的是 DSH 进程的process.env(HINDSIGHT_API_LLM_PROVIDER或OPENAI_API_KEY/ANTHROPIC_API_KEY/ …), 读不到~/.hindsight/profiles/<profile>.env里已经配好的值。检测不到就直接放弃, 日志里留下daemon mode needs an LLM for fact extraction。 - 它只等 12 秒。 会话启动时
seedIfCold用DAEMON_WAIT_SESSION_START_MS = 12e3等待, 且每个进程只尝试一次。而 daemon 冷启动要 ~30 秒(Python 侧import torch+import sentence_transformers就要 12~13 秒),根本等不到。
这个插件把"拉起 daemon"这件事从官方插件手里接过来:在 DSH 自己的 Cordis 树里 做,时机更早、不占用任何额外进程,而且退出时能干净收尾。
它是怎么工作的
DSH 启动
└─ Cordis 加载本插件 (lib/index.js → apply)
├─ 注册 /hindsight-daemon/api/* 路由(设置页用)
└─ 后台 spawn: hindsight-embed -p <profile> daemon start
└─ daemon (pythonw) 监听 127.0.0.1:9077
└─ pg0 拉起嵌入式 PostgreSQL (:5432)
DSH 退出(三条通道,命中任意一条就够)
├─ ① Cordis disposer:桌面端 main.js 的 before-quit 会 dispose 整棵树(宽限 5 秒)
├─ ② process 'exit' 钩子:进程正常结束 / 被信号终止且运行时有 signal-exit 时
└─ ③ SIGTERM / SIGHUP / SIGINT 钩子:宿主没注册信号处理器时兜底接管
└─ 清理动作(全程同步,信号钩子的约束):
├─ 掐掉 detached 的 hindsight-embed CLI
├─ 按端口找到 daemon PID 并杀掉
└─ pg_ctl stop -m fast -D <data> ← 优雅关库,下次不用回放 WAL
三条通道都只是入口,真正干活的是同一段幂等清理(shared.closed 只放行一次)。
实测:apply() 返回耗时约 2 ms(不阻塞 DSH 启动),清理约 500–600 ms,
远在 5 秒宽限之内。冷启动到健康约 20 秒(Linux,模型与依赖已就绪时)。
为什么需要三条通道:不同宿主给的退出保证不一样。
- Windows 桌面端会先拦下退出再 dispose 树,通道 ① 就够。
- Linux 上的
dshCLI 不注册任何信号处理器(实测process.listenerCount('SIGTERM')为 0), 被kill时进程直接终止,通道 ① 完全没机会跑。 - 但真实进程里往往已经有别的库(如
signal-exit)注册了信号监听器, 它会在终止前触发 Node 的exit事件 —— 所以通道 ② 才是 Linux 上实际生效的那条。 通道 ③ 只在「确实没人管这个信号」时接管,避免抢跑打断宿主的异步收尾。
清理必须显式做三件事,原因都在实测里踩过:
hindsight-embed daemon start这个 CLI 是 detached 的,daemon 被杀后它会把 daemon 再拉起来 —— 所以第一步先掐 CLI。hindsight-embed daemon stop不会停 PostgreSQL,而且实测要 4 秒,贴着 5 秒宽限,不安全 —— 所以改按端口 PID 强杀 daemon,再用pg_ctl stop -m fast单独关库(约 160 ms)。
环境要求
| 项目 | 要求 |
|---|---|
| 系统 | Windows 10 / 11,或 Linux(x86_64) |
| DSH Desktop | 2.0.x(依赖 apply() 返回函数即 disposer 的 Cordis 4 语义) |
| Node.js | ≥ 22(DSH 自身要求;scripts/install-deps.mjs 也用它) |
| uv | 由安装流程检查;缺失时会提示如何安装 |
| 磁盘 | 约 3 GB(Python 依赖 1.5 GB + 本地模型 1.2 GB + 数据库) |
| 模型服务 | 一个 [OI] 兼容的 LLM 接口(只用于抽取事实) |
安装
方式一:一键脚本(推荐)
git clone https://github.com/FYTXY/dsh-hindsight-daemon.git
cd dsh-hindsight-daemon
Windows(PowerShell):
powershell -ExecutionPolicy Bypass -File .\scripts\install.ps1
install.ps1 会追加 insert 块,并接着调用 scripts/install-deps.mjs 安装 Python 依赖与
两个本地模型(想跳过就加 -SkipDeps,改在设置页里点「一键安装」)。
可用的参数:
-Profile <名> DSH profile 名,默认 desktop
-DshHome <路径> DSH home 目录,默认 $env:DSH_HOME 或 %USERPROFILE%\.dsh
-SkipDeps 只挂插件,依赖留到设置页里点「一键安装」
Linux:
chmod +x ./scripts/install.sh
./scripts/install.sh # 默认装进 web profile
./scripts/install.sh --profile desktop # 或指定 profile
install.sh 只做一件事:在 profile 的 cordis.patch.yml 里追加 insert 块(写的是本仓库
lib/index.js 的绝对路径,所以 git pull 升级后立即生效)。依赖同样留给设置页。
两个脚本都是幂等的:重复执行不会写入第二个 insert 块。
装完重启 DSH,然后打开 设置 → Hindsight 记忆,填入你自己的模型服务。
方式二:手动挂载
把插件目录放到任意位置,然后在 <DSH home>/profiles/<profile>/cordis.patch.yml 末尾追加:
- insert:
- id: hindsight-daemon
name: '/absolute/path/to/dsh-hindsight-daemon/lib/index.js'
注意 name 用正斜杠(Windows 上也写 C:/path/to/...),指向 lib/index.js。之后在设置页点「一键安装」补齐依赖。
方式三:npm
dsh plugin --profile desktop add dsh-hindsight-daemon
package.json 里带了 dsh.bundle.patch,安装后由包自带的 cordis.patch.yml 自动挂载。
三种方式只能选一种。重复挂载会让
/hindsight-daemon/api前缀路由注册两次, DSH 启动时报webserver: duplicate prefix route。
设置页
设置 → Hindsight 记忆(导航里排在靠后的位置)包含六块:
每个按钮都有明确反馈:点击后按钮文字立刻变成
保存中…/启动中…/切换中…这类进行中状态并禁用,避免重复点击;操作结果紧跟在底部 保存配置 / 重新探测 两个按钮下方 —— 成功是绿色回执(如「配置已保存」),5 秒后自动消失;失败是红色错误,一直保留到下一次操作成功。
服务状态
显示阶段(已停止 / 启动中 / 运行中 / 安装中 / 异常)、地址与已运行时长,
并提供 启动 / 停止 按钮。每 2 秒刷新一次。
环境检测
分别显示 uv、hindsight-embed、PostgreSQL 与数据库目录的就绪状态。
任何一项缺失都可以用一键安装补齐。探测优先级:设置里填的路径 > 环境变量 > PATH > 常见安装位置。
一键安装
幂等,可以重复点。依次执行五个阶段:
| 阶段 | 做什么 |
|---|---|
check |
定位 uv |
packages |
uv tool install hindsight-embed --with hindsight-api --force |
profile |
创建 / 对齐 hindsight-embed 的 profile(写环境变量) |
models |
用 venv 里的 Python 通过 huggingface_hub 下载 embedding 与 reranker |
verify |
import torch, sentence_transformers, hindsight_api 自检 |
--with hindsight-api 这一步很关键:把 API 装进同一个 venv,daemon 启动时才能直接找到
hindsight-api.exe;否则它会退回 uvx hindsight-api@<version>,在临时环境里重新下载一遍 600 MB。
当前记忆
只读,回答「记忆到底存在哪」:数据库目录、占用空间,以及每个记忆库的节点 / 关联 / 文档数。
一个代码仓库对应一个记忆库,按仓库名自动命名,不同项目之间互不可见。这些信息不能在这里修改 —— 位置由下面的「高级」决定,想换地方见「文件都放在哪」。
记忆仓库(Profile)
列出本机所有 profile,标明哪个正在使用,并可以直接新建、切换、删除。
一个 profile 就是一间独立的仓库:自己的数据库、自己的 env(含网关密钥)、自己的日志。 新建只写 env 文件,不建数据库、不启进程 —— 等它被启用、daemon 第一次启动时才建出来, 所以新建本身不占磁盘。启用某个仓库会停掉当前 daemon 再按新仓库拉起(约 30 秒), 原来的仓库原封不动,随时能切回来。
名字只能包含字母、数字、点、下划线和连字符,以字母或数字开头,最多 32 个字符。 只允许切到已经存在的 profile —— 手输一个拼错的名字会静默建出空库,比直接报错更难察觉。
删除:每个仓库右侧(「启用」之后)有删除按钮,点一下会先弹确认框,确认后才会真的删。 删除是不可恢复的,会连数据库目录、env 文件(含网关密钥)、日志和登记表条目一起清掉; 正在使用的那一个按钮是灰的 —— 想删它,先把别的仓库切成当前使用。 删除不会碰其他仓库,也不会停 daemon。
模型服务
| 字段 | 说明 |
|---|---|
| Provider | 目前走 OI 兼容协议,选 openai 即可 |
| 模型名 | 抽取事实用的模型,例如 gpt-4o-mini |
| Base URL | 兼容网关地址,例如 https://api.example.com/v1 |
| API Key | 落盘明文保存;设置页只回显圆点(••••)表示已设置,留空保存表示不修改 |
填完 Base URL / 模型名 / API Key 之后,点 测试连通性 会立刻用当前草稿发一次最小请求,
直接告诉你通不通、失败原因、耗时和接口实际回显的模型名 —— 不必等到写记忆时才发现配错。
密钥留空时自动回退到已保存的那一份,所以「刚填完还没保存就想测」也能用。
测试只读,不写配置、不影响正在运行的 daemon。
高级
| 字段 | 说明 |
|---|---|
| Profile 名 | 只读,显示当前使用的仓库。要换仓库请用上面的「记忆仓库(Profile)」列表 —— 直接改名等于指向一个全新的空库:旧记忆不会被删除,但也不会跟过来 |
| 记忆库命名模板 | 留空 = 每个代码仓库自动一个库,模板为 coding-agent::{gitProject}。填了会当成字面模板 —— 填 work 就是所有项目共用 work 这一个库 |
| 本地端口 | daemon 监听端口,默认 9077,改动需重启 DSH |
| HF 镜像 | 默认 https://hf-mirror.com,只影响首次下载模型,装好后不再起作用 |
| PyPI 镜像 | 默认清华源,只影响安装依赖,装好后不再起作用 |
前两项决定记忆存在哪,后三项只影响下载与监听 —— 所以界面上把危险项排在前并单独加了警示。
记忆库到底叫什么
上游插件按模板算库名,默认模板是 coding-agent::{gitProject},{gitProject} 取 git 仓库根目录名:
| 当前工作区 | 推导出的库名 |
|---|---|
~/projects/my-app(git 仓库) |
coding-agent::my-app |
~/scratch/notes(不是 git 仓库) |
coding-agent::notes(取目录名) |
可用占位符:{gitProject}(git 仓库根目录名,worktree 会追到主仓库)、{project}(当前目录名,不追仓库根)、{harness}、{channel}、{user}。
所以换一个工作区目录,只要它是另一个 git 仓库,就会自动写进另一个库 —— 互不串味,不需要任何配置。库里没东西就是还没在这个仓库里聊过天。
页面底部合并展示插件日志与 daemon 日志(各取尾部),排错时先看这里。
文件都放在哪
| 内容 | 路径 |
|---|---|
| 插件配置 | <DSH home>/hindsight-daemon/config.json |
| profile 环境变量 | ~/.hindsight/profiles/<profile>.env |
| hindsight 全局配置 | ~/.hindsight/coding-agent.json |
| daemon 日志 | ~/.hindsight/profiles/<profile>.log |
| 数据库 | ~/.pg0/instances/hindsight-embed-<profile>/data |
| Python 运行时 | <UV_TOOL_DIR>/hindsight-embed(默认 ~/.local/share/uv/tools,Windows 为 %LOCALAPPDATA%\uv\tools) |
| 本地模型 | $HF_HOME,未设置则在 ~/.cache/huggingface |
上表用 ~ 表示用户主目录:Windows 上是 %USERPROFILE%(C:\Users\<你>),Linux 上是 $HOME。
卸载
powershell -ExecutionPolicy Bypass -File .\scripts\uninstall.ps1
chmod +x ./scripts/uninstall.sh
./scripts/uninstall.sh # 默认从 web profile 移除
./scripts/uninstall.sh --profile desktop # 或指定 profile
只从 cordis.patch.yml 里移除 insert 块(同样会备份)。重启 DSH 后插件不再加载。
Python 依赖、模型与数据库都保留,方便之后重新挂上;要彻底清理请按上面表格手动删除对应目录。
常见问题
首次启动很慢? 正常。首次要下 1.5 GB 依赖 + 1.2 GB 模型,之后冷启动约 30 秒。装好依赖和模型后, 每次启动就只是 Python 导入的开销。
日志里出现 daemon mode needs an LLM for fact extraction?
这是官方 @vectorize-io/hindsight-coding-agents 插件在报怨它自己检测不到 LLM —— 它只看
DSH 进程的环境变量。本插件不依赖那条路径,看到这条可以忽略,只要 daemon 状态是「运行中」即可。
依赖安装到一半失败? 装依赖对网络比较敏感。默认已经走清华 PyPI 镜像和 hf-mirror;如果在公司网络里,把 「PyPI 镜像」和「HF 镜像」改成内网地址,或留空走官方源。装到一半失败可以直接重试, 已下载的轮子有缓存。
端口 9077 被占用?
改「本地端口」并重启 DSH。插件启动前会先探测 /health,如果端口上已经有一个健康的
daemon(可能是你自己手动起的),会直接复用而不是再拉一个。
改了端口 / Profile 名没生效? 这两项要重启 DSH 才生效(要重建 profile 与全局配置)。
DSH 非正常退出(崩溃 / 强杀)后 daemon 还在?
说明清理通道都没跑到(例如被 SIGKILL,任何语言都拦不住)。手动收一次即可:
# Windows
taskkill /F /IM hindsight-embed.exe /T
Get-NetTCPConnection -LocalPort 9077 -State Listen | ForEach-Object { taskkill /F /PID $_.OwningProcess /T }
& "$env:USERPROFILE\.pg0\installation\*\bin\pg_ctl.exe" stop -m fast -D "$env:USERPROFILE\.pg0\instances\hindsight-embed-<profile>\data"
# Linux
pkill -f hindsight-embed
pgrep -f "hindsight" | xargs -r kill -9 # 或按端口:ss -ltnp | grep 9077
"$HOME/.pg0/installation/"*/bin/pg_ctl stop -m fast -D "$HOME/.pg0/instances/hindsight-embed-<profile>/data"
下次启动 DSH 时如果数据库没干净关闭,PostgreSQL 会自己回放 WAL 恢复,不会丢数据。
想看清退出路径到底哪条通道生效?
设置环境变量 DSH_HINDSIGHT_DEBUG=1 再启动 DSH,插件会把钩子注册与清理决策写进
~/.dsh/hindsight-daemon/debug.log:
退出钩子已注册 pid=58065 SIGTERM=2 SIGINT=2 exit=3
收到 SIGTERM,listeners=2 pid=58065
cleanup(exit) enabled=true stopOnExit=true busy=null
listeners 数量很关键:大于 1 说明宿主/依赖已经有人处理这个信号,信号钩子会让位,
真正的清理走 exit 通道;等于 1 时才是信号钩子自己接管。
升级插件后行为没变?
多半是加载了另一份旧副本。DSH 的 profile 有两条挂载路子 ——
dsh.profile.bundles(按包名从 node_modules 解析)和 profile 自己的 cordis.patch.yml
(可以写绝对路径)。如果 cordis.patch.yml 里留着指向源码目录的绝对路径,它会覆盖
bundle 那条,于是 dsh plugin add 装的新版本根本没被加载。检查:
grep -rn "hindsight" ~/.dsh/profiles/<profile>/cordis.patch.yml
推荐只留一种:把 name 写成包名 dsh-hindsight-daemon,让版本跟着 dsh plugin add 走。
开发
node --check lib/index.js # 语法
node scripts/install-deps.mjs # 在 DSH 之外跑一遍安装流程,便于调试
目录结构:
lib/
index.js Cordis 入口:什么时候挂接口、什么时候拉起、什么时候收摊
lifecycle.js 生命周期:探测 → 启动 → 停止 → 安装 → 保存配置
api.js /hindsight-daemon/api/* 路由 + 同源围栏
config.js 配置读写(原子写、密钥脱敏、扁平化转换)
client.js 浏览器半边:设置页
core/
paths.js 磁盘探测:uv / hindsight-embed / pg_ctl / 数据目录
daemon.js 健康探测、按端口找 PID、spawn、三段式关闭
profile.js profile 创建与 env 行级补丁、全局配置写入
installer.js 五阶段安装(流式读取子进程输出)
status.js 状态与日志环形缓冲
scripts/
install.ps1 Windows 一键安装(写 cordis.patch.yml + 跑依赖安装)
install.sh Linux 一键安装(只写 cordis.patch.yml)
uninstall.ps1 Windows:从 cordis.patch.yml 移除
uninstall.sh Linux:从 cordis.patch.yml 移除
install-deps.mjs 在 DSH 之外执行依赖与模型安装
设计上刻意零运行时依赖:配置自己读写,设置页自己渲染,不引 schemastery /
dsh-shared / dsh-api-gateway。原因是这些包在公开 npm 上的版本(0.0.1-rc.1 系列)
与本机 DSH 内置版本(0.2.0-rc.2)不一致,绑定它们会让插件在 DSH 升级后随时崩掉。
No comments yet. Be the first to write one.