coding-agents
vectorize-io/hindsightHindsight:会学习的 Agent 长期记忆系统,自动召回/保存、知识页、深度反思与按仓库隔离的记忆银行。
跨会话、跨工程的长期记忆:turn/end 时非阻塞派发无感静默沉淀,写入本机 SQLite(FTS5 trigram 全文索引,含增删改同步触发器)供后续回合召回;记忆按项目作用域与全局树分层存储,并自带内嵌 Web 看板(插件自身起 HTTP 服务,非独立进程)用于浏览、图谱与受控删除。
DeepSeek Harness (DSH) 原生无感记忆与自省管理插件。
为 DeepSeek Harness 提供会话级静默记忆提炼持久化、SQLite FTS5 全文检索,以及与 DSH 官方 Shell(如 SSH、任务看板)100% 深度融合的一级主视口记忆看板。
1.0.0 起适配 DSH 0.2.1-rc.2:新增设置页「记忆」面板(dsh.client 契约 + external: ["react"]),八个配置项全部有默认值、零必填,保存后立即生效。
remote.settings 并带 revision 乐观并发,多窗口同时改会提示冲突并回填最新值;外部(补丁层 / 另一窗口)改动经 settings/document-updated 实时反映到面板。confirmed 入库,疑似指令式规则只记一条 warn 日志便于追溯,不再进待确认区等人点确认;矛盾检测发现旧结论被新结论推翻时,旧条自动置 superseded(退出召回、原文留档、merged_into 记录取代者、可逐条 restoreSuperseded 回滚)。同一轮还会做确定性目录归并(mergeSimilarDirectories):同父目录下名字近义(二元组 Dice ≥ 0.55)的兄弟目录合成一棵,只改挂载位置与路径前缀,不动任何 content,零 LLM 成本、可重复执行。想恢复人工把关,把 requireReview 打开即可。merged_from 留档可追溯;被并掉的叶子置 merged_into 后不再召回、在看板显示「已并入」。全部走保守口径 —— 宁可少合,也不把不同主题搅在一起。默认开启(enableAutoMerge: true,每轮 compaction 最多折叠 8 簇),不想要可在设置页关掉;想先看会合并成什么,用 tlmemory_consolidate 的 dry_run 或 pnpm --filter dsh-plugin-tlmemory preview:merge。workspace.json 的第一个工作区(workspaceIds[0],往往是一个与当前工作毫无关系的旧仓库),装配期还会按「同名对齐 / 白名单首位」把存量记忆整体搬过去。后果是各工程的记忆被逐条搬进同一棵树 —— 用户侧表现为「某个工程莫名其妙攒了一千条,自己真正在用的工程只有几条」。1.2.0 起记忆永远留在它自己的作用域:名单外目录至多让看板多出一个工程,绝不再把 A 工程的记忆塞进 B 工程;装配期的自动归并一并移除(db.mergeOrphanScopes 只保留给显式修复调用)。看板默认落点也从「记忆最多的工程」改成「最近活跃的工程」,避免一打开就撞进历史错账树。session/event 契约监听 turn/end (completed) 事件,静默后台提炼关键工程结论、用户偏好与决策规则,不抢占推理上下文。‹ 返回会话 快捷回退。repo:<hash> 反解为可读工程名,同时标注所属工作区(工程名 [工作区名] (N条)),切换即换树。归属判定对存量数据友好:同一目录的历史 hash 变体(盘符大小写、正/反斜杠写法差异)全部认账,工程名与某个合法工作区同名的存量工程也自动对齐到该工作区。scope 是否出现在 ~/.dsh/storages/workspace.json 的名单里,都照常保留并展示 —— 白名单永远不会成为「隐藏或删除用户记忆」的理由。工程名 (0)),选中即进入「暂无记忆,点击上方「+ 新建记忆」开始沉淀」的空状态,作为「当前工程就绪、可随时沉淀」的心智锚点;同名工程(不同路径的同名仓库、目录改名遗留的旧作用域)自动追加 scope 短标识收敛为唯一名,手工重命名撞名时明确报错。GET /api/nodes 回 HTTP 200 + 空记忆树 + 可读提示(而不是 404),看板按空态渲染;下拉框在任何状态下都可以唤起,且选中的工程若不在当前清单里会自动回退到最近活跃的工程(1.2.0 起;旧口径是「第一个有记忆的工程」,会永远落进条数最多的历史错账树),不会出现「暂无工程记忆 + 下拉点不开」的死锁。52+22px(竖向要 208+22px),真实记忆树这种「宽而浅」的形状因此能放大 1.5~1.8 倍(实测同一作用域 25%→42%、37%→68%、49%→89%);「一键居中」另有可读保底(0.6)——装不下整棵树时不再缩成 25% 的一团灰,而是把根节点钉在左上角、由用户拖拽浏览局部。tlmemory_delete 工具 —— 沉淀错了的记忆模型可以直接删掉,但必须显式 confirm: true、只允许删叶子(目录级联删留给看板)、默认只能删当前工程作用域(删 global 或其它工程需显式 allow_cross_scope: true);tlmemory_query 的每条结果现已附带节点 id,模型据此精确定位,同名多条会返回候选而不会猜着删。maxTokens 截断时不再整轮丢弃 —— 字符串感知的平衡扫描定位真正的 JSON 收尾、截断处回退到最后一个完整条目并补齐闭合括号,裸数组 / 单条对象 / 散文夹多个对象都能归一;解析彻底失败会同时写宿主 logger.warn 与 ~/.dsh/tlmemory-extract.log,不再无声吞掉。本包是 DSH 原生 bundle 形态:package.json 的 dsh.bundle.patch 指向包内自带的
cordis.patch.yml。被列进 profile 的 dsh.profile.bundles 即自动激活,不需要再往
profile 的 cordis.patch.yml 写任何挂载条目。
为什么必须是 bundle(0.7.0 的修复):桌面端(Electron carrier)与
@deepseek-ai/dsh-plugin-manager只认dsh.bundle.patch这一条激活通道 —— 安装时会以not-a-bundle(<包名> declares no dsh.bundle)直接拒绝, 归并时也只会把它当成「普通依赖」而不写进dsh.profile.bundles。 0.6.x 及更早版本只声明dsh.client,在桌面端根本无法被挂载。
为什么换掉 better-sqlite3(0.8.0 的修复):桌面宿主是用 Electron 自带的 Node 跑起来的(
process.execPath+ELECTRON_RUN_AS_NODE=1,实测 Node v24.18.1 /NODE_MODULE_VERSION 149),而 pnpm 是在系统 Node(v24.12.0 / ABI 137)下编译 原生模块的 —— 于是宿主挂载插件时dlopen直接失败:was compiled against a different Node.js version using NODE_MODULE_VERSION 137. This version of Node.js requires NODE_MODULE_VERSION 149.; 插件激活不了、4890 端口没人监听、看板一直显示「服务未启动」。 0.8.0 起持久层改用 Node 内置的node:sqlite(零原生依赖,跨 Node / Electron 稳定), 因此 运行需 Node >= 22.13(node:sqlite自 v22.13.0 / v23.4.0 起不再需要--experimental-sqlite),安装时也不再需要allowBuilds放行原生模块。
dsh plugin --profile web add dsh-plugin-tlmemory
dsh plugin add 会自动完成:装依赖 → 把包名归并进 dsh.profile.bundles(幂等)。
因为是 bundle 形态,不会再往 cordis.patch.yml 追加挂载条目;0.8.0 起包内已无原生
模块,安装过程不会再有 Ignored build scripts 需要放行。
另外 dsh plugin --profile web list 可查看挂载状态,dsh plugin --profile web remove <包名>
可一键卸载。
桌面端(DSH 桌面应用):profile
desktop由 Electron 应用独占管理,dsh --profile desktop …与dsh plugin --profile desktop …都会被拒绝 (error: profile "desktop" is managed exclusively by the Electron application)。 请直接在桌面端的插件管理界面安装 / 启用dsh-plugin-tlmemory—— 它同样只按dsh.bundle归类,因此 0.7.0 起可以被正常识别为可激活的插件层。
桌面端看板仍显示「服务未启动」?(0.8.1 的修复):桌面端主窗口文档来自
dsh-app://app/…(Electron 自有协议),渲染层 Origin 是dsh-app://app,属非回环 HTTP。 0.8.0 及更早的 CORS 白名单只回写 127.0.0.1 / localhost,于是外壳对看板的跨源探针被 CORS policy 拦掉、读不到响应,被误判成离线 —— 但宿主侧其实已经起来了(可自行验证:curl http://127.0.0.1:4890/api/health返回{"ok":true,"service":"tlmemory"})。 0.8.1 起白名单一并放行dsh-app:协议;null/file:/ 其它自定义协议仍然拒绝。 升级后请重启桌面应用。
记忆归属修复(0.8.2):三处写入路径的行为变更 ——
- 宿主工作区白名单改为实时读取(旧版在插件装配那一刻冻结快照):宿主运行期间 新建 / 改名的工作区(含中文名工作区)立刻生效,不会再被「防漂移」改道写进白名单 首位那个无关工程,导致该工作区零记忆、随后被维护周期摘出看板清单;
- 提炼提示词携带当前项目身份(工程名 / 根目录 / scope),
global判据收窄为 「跨工程通用的用户偏好 / 习惯」;tree = "global"只在同时给出tree_basis = "cross-project-preference"时生效,否则一律落回当前工程 (tlmemory_save(tree_scope=global)这条显式通道不受影响);- 看板
html/body禁止横向滚动(overflow-x: hidden),打开记忆详情不会再出现 整页横向位移。 升级后请重启宿主;pnpm run verify:drawer可用真实浏览器复测抽屉几何(PASS / FAIL)。
--profile必须紧跟dsh plugin,且只能出现一次:dsh plugin --profile web add --profile=1这类写法会显式报错并拒绝执行(旧版会静默丢弃参数、同时把整条命令改道到另一个 profile)。 命令的失败以稳定退出码分类:0成功 /1用法或前置条件不满足 /2pnpm 缺失 /3清单非法 /4归并不一致 /124pnpm 超时,其余透传 pnpm;每条诊断行都带dsh: diagnose: <code>:前缀,便于脚本与 agent 直接匹配。本层全局选项(任何子命令都可用,且不会转发给 pnpm):
选项 作用 --jsonstdout 只输出一行结果 JSON( phase/exitCode/pnpm/addedBundles/mounts/allowBuilds/diagnostics…),人类日志改走 stderr;也可用DSH_PLUGIN_OUTPUT=json--timeout <值>pnpm 超时( 1500/30s/5m),超时以124结束;也可用DSH_PLUGIN_TIMEOUT--yes确认「用内置默认层栈首次创建无模板的同名 profile」;没有它时未知 profile 名的首次创建会被拒绝(避免手误留下半成品 profile) --no-lock跳过 profile 互斥锁 其它内建保障:profile 自己是 workspace root 时自动给
add/remove/update补-w; 会写盘的子命令默认持有<profile>/.dsh-plugin.lock;回写package.json前先存.bak-dsh-plugin-manifest-<时间戳>并原子替换;pnpm 的输出被实时转发同时被捕获, 失败时给出分类后的诊断(adding-to-root/ignored-builds/fetch-404/windows-file-locked/network…)。
进入你的 DSH profile 目录(例如 ~/.dsh/profiles/web):
cd ~/.dsh/profiles/web
# 从 npm 安装
pnpm add dsh-plugin-tlmemory
0.8.0 起持久层走 Node 内置的 node:sqlite,没有原生模块需要编译,也不需要放行
allowBuilds;请确保宿主 Node >= 22.13。装完直接把包名加进该 profile 的
package.json —— 只装依赖、不写这一行是不会生效的:
"dsh": { "profile": { "bundles": ["…", "dsh-plugin-tlmemory"] } }
本插件没有任何必填项 —— 不写配置、不建目录、不预先建库,宿主启动时会自动:
~/.dsh/tlmemory.db(<主目录>/.dsh/tlmemory.db)创建 SQLite 库文件;4890 启动记忆看板。所以最小可用配置就是没有配置:
# 无需任何配置项:包被挂载即工作。
dsh web → 设置 → 左栏 「记忆」:
| 配置项 | 默认值 | 说明 |
|---|---|---|
dbPath | 空 = ~/.dsh/tlmemory.db | SQLite 数据库路径。清空即回到默认路径 |
serverPort | 4890 | 看板与本机 REST API 的监听端口(仅回环地址) |
maxRecallCount | 5 | 每轮最多注入系统提示词的记忆条数 |
enableAutoReflection | true | 会话结束是否后台自动提炼沉淀 |
compactionInterval | 20 | 每累计 N 次沉淀触发一轮强化衰减 + 矛盾检测(0 表示不自动触发) |
enableAutoMerge | true | 把同一主题的多条短记忆折叠成一条摘要(原文留档、可回滚) |
enableAutoEvolve | true | 自动整理记忆树:合并近义目录,让结构随使用收敛(只改结构,不动内容) |
requireReview | false | 是否保留人工审核。默认关闭 = 沉淀直接入库、旧结论自动淘汰;打开后疑似注入式规则与近似重复会进「待确认区」 |
改动保存即生效,无需重启宿主:配置写入 profile 后,宿主会热重载本插件并以新配置重新装配
(数据库按新 dbPath 重新打开)。设置页按 revision 做乐观并发,多窗口同时改会提示冲突并
自动载入最新值,不会带着旧版本号盲写。
想手工管理配置就在自己的 profile 补丁层 ~/.dsh/profiles/web/cordis.patch.yml 里按 id
覆盖(写 - id: 覆写行,不要再写 - insert:):
- id: dsh-plugin-tlmemory
config:
serverPort: 4890 # 看板服务端口(默认 4890)
maxRecallCount: 5 # 单轮最多注入系统提示词的记忆条数
enableAutoReflection: true # 会话结束异步自动反思提炼
compactionInterval: 20 # 每累计 N 次沉淀触发一轮强化衰减 + 矛盾检测
dbPath: ~/.dsh/tlmemory.db # 可省略;省略即用这个默认路径
设置页与补丁层写的是同一份 profile 配置:GUI 保存后,补丁层里对应的
config:段会被更新, 反之手工改动也会通过settings/document-updated事件反映到设置页。
⚠️ 从 0.6.x 升级:先删掉旧的手工挂载条目
0.6.x 不是 bundle,只能靠 profile 补丁层里的
- insert:挂载,而且当时文档给的 id 是tlmemory-runtime(不是包名)。Cordis 按 id 去重而不按 name,所以旧的tlmemory-runtime条目和 0.7.0 的 bundle 条目会同时生效 —— 同一个包被挂载两次, 结果是两个数据库句柄、两个抢 4890 的监听实例。升级步骤:
- 在 profile 的
cordis.patch.yml里删掉整段- insert: - id: tlmemory-runtime …;- 把
dsh-plugin-tlmemory加进dsh.profile.bundles(dsh plugin add会自动做);- 需要自定义配置时,按上面的
- id: dsh-plugin-tlmemory覆写行写。
端口冲突自愈:4890 被前序 tlmemory 实例占用时,新实例会经健康探测确认同名进程 后自动复用(多宿主并存无需手工分工);被无关进程占用时自动顺延端口;连续顺延 仍失败时在控制台打印 EADDRINUSE 排查指引。
禁区端口自检:浏览器与
fetch共同封禁的端口(如 3659 / 4045 / 4190 / 5060 / 5061 / 6000 / 6566)无法打开看板也无法被 HTTP 客户端访问,绑定后会被自检识别并 自动换端口重试 —— 配置serverPort: 0交由系统分配时同样保证落在可用端口上。
在任意终端路径下直接启动:
dsh web
Node 版本:运行需 Node >= 22.13 —— 0.8.0 起持久层用 Node 内置的
node:sqlite(v22.13.0 / v23.4.0 起不再需要--experimental-sqlite,v24.2.0 起不再 experimental), 因此不再有原生二进制,也就不再有NODE_MODULE_VERSION不匹配这类问题。
| 工具 | 用途 | 关键约束 |
|---|---|---|
tlmemory_save | 显式沉淀一条记忆 | content 截断到 80 字;tree_scope 决定落 global 树还是当前工程树 |
tlmemory_query | FTS5 全文检索既有记忆 | 结果已附带节点 id,可直接交给删除工具精确定位 |
tlmemory_delete | 删除一条记忆(不可逆) | 必须 confirm: true;仅叶子(目录级联删请走看板);默认只删当前工程,global / 其它工程需 allow_cross_scope: true;同名多条返回候选要求改用 id |
tlmemory_consolidate | 归并同主题的碎片记忆(默认只预览) | dry_run 默认 true,返回簇清单不写库;dry_run:false 才真正合成并留档 merged_from;每簇 2–8 条、失败不写库、模型缺失静默跳过 |
看板上的删除按钮与待确认区的「拒绝」是同一套底层能力(
DELETE /api/nodes/:id, 由trg_nodes_ad触发器同步 FTS5 索引);Agent 工具只是把这条路径按「必须显式确认 + 作用域隔离 + 仅叶子」收窄后开放给模型,避免一次措辞含糊的调用毁掉正确记忆。
# 运行单元测试(全工作区)
pnpm run test
# 构建全部产物:dist/(Node 核心)+ web/client.js(客户端插件)+ web/dist/(看板前端)
pnpm run build
# 归并 dry-run:只读库 + 同一套聚类规则,打印「会合并成什么」(--all 全量 / --json 管道)
pnpm --filter dsh-plugin-tlmemory preview:merge
# 客户端 Bundle 真实冒烟走查(jsdom 加载真实产物,需先 build)
pnpm run smoke:client
# dsh CLI 增强层的单测 / 安装(可选,见 tools/dsh-plugin-cmd)
pnpm run test:cli
pnpm run install:cli
根目录
pnpm run build是递归构建(workspace 同时包含packages/tlmemory与packages/tlmemory/web),因此dist/、web/client.js、web/dist/会被一并刷新 —— 发包前跑这一条即可。单独构建看板前端用pnpm --dir packages/tlmemory/web run build。
MIT License