# 安全说明:dsh-reclaim 凭什么认为一条数据可以删 > **状态**:这是 `dsh-reclaim` 的**实际安全契约,已实现并验证**(尚未发布到 npm)。下面是它**现在**遵循的规则,不是打算遵循的规则——每一条都能在代码里对照。 `dsh-reclaim` 会删除你的数据。所以本文不是宣传材料,而是**可被审计的规则说明**:你应该能自己判断它会不会删错东西。 --- ## 一、为什么需要它:官方明确不做回收 DSH 的附件存储(官方包 `@deepseek-ai/dsh-attachment-local`)在 README 里写得很清楚: > **Images are kept forever** — stored images are never deleted automatically, and nothing collects unreferenced objects. 官方也说明了为什么不回收(同一份 README 的 Dev Note): > Retention and garbage collection are deferred because **resumed and forked sessions may share immutable objects**, and a backend serving remote runtimes or shared storage would need its own durability proof. 意思是:这个功能被**有意推迟**,原因是"恢复和分叉出来的会话可能共享同一批不可变对象"。这不是"忘了做",而是一个需要先解决正确性问题的功能。 `dsh-reclaim` 就是把这个回收做出来,并且**严格尊重官方指出的那条约束**:引用计数跨 fork 生效。 --- ## 二、它只处理三类东西 | 类别 | 具体是什么 | |---|---| | **会话** | `/sessions/` 下的会话日志 | | **附件** | `/attachments/v1/` 下的图片与文件对象 | | **可重建缓存** | 派生的"请求版"图片、会话投影缓存、暂存残留 | **它不碰**:日志文件、工具输出溢出的临时文件、备份目录、`profiles/` 下的插件依赖(后者只报告占用,不代删)。 --- ## 三、判定只看信号,不看猜测 ### 三条铁律 1. **引用关系只认结构化字段。** 判定"这张图还有没有人在用",读的是会话记录里明确写着 `attachmentId` 的字段,**不是在日志文本里搜哈希**。 > 为什么强调这一点:会话日志里包含完整的工具输出。如果在文本里搜哈希,任何一次"打印目录列表"都会被误读成"引用了这些文件"。这类假阳性是文本搜索方案的固有性质,所以本项目从设计上就排除了它。 2. **判定相对于"这一批保留下来的东西"。** 不是看"现在有没有人引用",而是看"删掉这一批之后还有没有人需要它"。 3. **不确定就停手。** 只要有任何一个会话的引用关系**无法被证明完整**,「孤儿附件」这一档就整体暂停,并在界面上明说原因。 "无法证明完整"包括三种情况:读取报错;**读取成功但没解析出应有的内容**;日志被截断。后两种**都不会报错** —— 所以插件读完之后会做两道对账(事件数、引用数),对不上就按未知处理。**宁可少删,不可删错。** ### 分档规则 | 档 | 会话 | 附件 | |---|---|---| | **安全**(可批量清理) | 已归档 **且** 超过阈值天数未活动 **且** 无任何入边 **且** 不是任何保留会话的父会话 **且** 其子会话都在本批内 —— 五个条件**全部**满足 | 零引用的孤儿;`request-images/` 派生缓存(可重新生成);`tmp/` 残留 | | **需确认**(逐个查看) | 未归档;或已归档但不够久;或零引用但未归档;或它是某个保留会话的父会话 | 仅被"本批待删会话"引用的(走显式的连带清理选项,默认不勾) | | **危险**(不可清理) | 正在运行;**已加载在内存中**;被保留集会话 @引用;是某个**不在本批**的会话的子会话 | 被任何保留集会话语引用;**不属于任何已定义族的对象**(不知道它是什么,因此不提供清理) | | **未分类**(只报告) | — | 目录**没能枚举**(读不到或后端不可用)的族:既不说它安全,也不说它有什么 | **「已归档」是信号,不是授权。** 单独归档不足以放行,必须五个条件齐全。 > **"不知道"一律不入安全档,但要分清两种不知道。** 不属于任何已定义族的对象(例如别人往 `attachments/` 里放的东西)**进危险档**——它不是缓存、不是本插件认识的任何东西,没有清理入口。而一个族**没能枚举**(目录读不到)时,它连"里面有什么"都不知道,进**未分类**档:只报告,不参与任何清理。两者的共同点是都不会被建议删除;区别是这个对象"不该动"还是"这次没读到"。 > **危险档为什么同时看两个信号**:「正在运行」和「已加载在内存中」不是一回事 —— 一个会话可能已经加载、正等着你输入,但此刻并没有在跑。这两种状态都必须被挡在清理之外,所以插件分别判定,并且**在执行删除前重新读一次实时状态**,不依赖任何更早的扫描结果。 ### 每一行给你两个数字 - **用了几张图** —— 这个会话引用了多少张(去重后) - **删掉可释放多少** —— 真正会变成孤儿的那部分 这两个数字不同,因为附件是**按内容去重**的:两张一样的截图只存一份,可能被多个会话同时引用。界面写"删掉可释放"而不是"占用",就是为了不在有共享时给出骗人的数字。 --- ## 四、删掉的东西去哪了:隔离区,可恢复 删除**不是**直接抹掉,而是搬进插件自带的隔离区: ``` $DSH_HOME/.dsh-reclaim-trash/<时间戳>/ ├── manifest.json # 原路径、类型、大小、时间,以及搬运前算出的风险档与证据 ├── sessions/... └── attachments/... ``` - 可以**按批次恢复**到原位 - 原位置已被新内容占用时**不覆盖**,报错让你决定 - 界面里可以**彻底清空**(永久删除,需二次确认) **隔离区自己占的空间也会显示在面板顶部** —— 否则你清了一轮会发现总占用没降。 **「清理所选」(可恢复)和「彻底清空」(永久)在视觉与措辞上是分开的。** 这是本插件最容易造成数据丢失的地方,所以刻意不做成相似的两个按钮。 --- ## 五、已知边界(使用时需要知道) | 边界 | 说明 | |---|---| | **扫描会读完整会话日志** | 官方说明"精确读取会重放整个日志",因此大历史下分析引用关系需要时间 | | **「正在使用」只在同一个进程内可见** | 判断一个会话是否"已加载"的依据是"它在这个进程的内存里"。所以**在 A 宿主里打开面板,看不到 B 宿主里正在进行的对话**:那个会话会被读成"未加载"。实践中这不会让你误删正在用的对话——批量清理还要求"已归档且闲置超过阈值",而正在用的会话两条都不满足——但如果你同时开着多个宿主,请只在你**正在使用**的那个里做清理 | | **引用完整性只能相对于可读的日志** | 日志被截断时读取不会报错,所以"已证明完整"的意思是"相对于能读到的部分完整"。凡是有会话读不通,孤儿附件这一档就整体暂停 | | **归档状态来自宿主自己的归档集合** | 那是宿主维护的偏好,不是文件属性。插件读不到时按"未知"处理,不会当成"未归档"也不会当成"已归档" | | **删除一个会话会同时清掉它的投影缓存** | 两者是同一份数据的两种形态,因此按"一个会话的两个条目"一起处理,而不是当成两件独立的东西 | --- ## 六、它不做的事 - 不清理日志 / 临时文件 / 备份目录 - 不删除或管理 `profiles/node_modules` 里的插件依赖 - 不集成系统回收站 - **不允许模型替你删数据**:Agent 侧只有一个**只读**的审计工具;破坏性操作必须由人在界面里完成 - **不做定时自动清理**:没有任何后台任务会删除东西;永久删除只发生在有人在界面上明确确认之后 --- ## 七、许可 MIT