Files
deepseek-harness/.agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md
2026-07-30 14:56:39 +08:00

8.3 KiB
Raw Blame History

Agent Note存储根目录落点与派生介质恢复

Status: proposed

English | 中文

Problem

持久投影缓存(RFC,已作为 dsh-session-projection-cache 落地)暴露了它所依托的存储基座的两个缺口。二者都是 domain-KV 栈(设计)的属性而非缓存自身的问题,且都首先咬到缓存——因为它是这条栈上第一个派生介质。

文件到底存在哪。 出厂 Web overlay 给 json 后端的是相对根目录——root: './.storages'apps/cli/config/web.cordis.yml)——而共享 base 将会话存储默认为全局 harness home$DSH_HOME/sessions,默认 ~/.dsh/sessionsstorage-json 没有对应的全局根目录。JsonStorageBackend 自己也从不 resolve 根——每次打开 unit 都把仍然相对的路径 join 到当时的 process.cwd()packages/storage/storage-json/src/index.ts——这正是 JSONL 会话后端用「构造时 resolve 一次」防住的那个隐患("later process.cwd() changes cannot split one backend across roots"packages/session-persistence/session-persistence-jsonl/src/index.ts。净效果会话日志跨启动目录全局共享workspace.jsonsession_projcache.json 落在 <启动目录>/.storages/ 下。从两个不同目录启动,会话相同,工作区注册表和投影缓存却各是一份——而缓存存在的意义恰恰是跨会话冷列表,如今凡是上次在别的启动目录下缓存过的会话全部 miss。

现在是怎么恢复的。 在健康介质内部,缓存按设计完全自愈:stateVersion 不匹配的行被丢弃重折,日志缩短到行水位以下由带锚的 restore floor 检出并以一次全量重读回答,每次后台写都是 fail-soft。但在介质层面完全没有恢复:被截断、被手改或版本被 bump 的 session_projcache.json 会让 openJsonUnitmalformed-medium/version-mismatch 失败packages/storage/storage-json/src/format.tsschema 漂移的记录让域 open 以 invalid-record 失败packages/storage/storage-domain/src/index.ts拒绝一路穿过 SessionProjectionCache[Service.init],在 CLI 的 fail-loud 启动下整个组装拒绝启动。一个内容完全可从会话日志重建的文件能把启动搞死。这与缓存包自己声明的立场("a stale or unreadable cache costs a longer tail replay, never a wrong value")和缓存域 spec 的 JSDoc"version bumps discard the whole medium")相矛盾——后者今天描述的是愿望而非实现。同一条 fail-loud 路径对 workspace.json 却是正确的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。

Proposal

两个独立改动,一个缺口一个。

全局唯一存储根,构造时 resolve 一次

  • AppCLIEntry.composePatches 的 Source 0 追加把 storage-json.root patch 到 join(resolveDshHome(), 'storages')——默认 ~/.dsh/storages,与 ~/.dsh/sessions 并肩——并且 PROFILE_MAPPINGS 增加 storageRoot →(storage-jsonroot),与 persistenceRoot 完全镜像。yml 保留 ./.storages 作为裸组合的工程默认(测试和裸 Loader 启动不受影响),分层方式与今天的会话根相同。
  • JsonStorageBackend 在构造时对配置根 resolve 一次,原样采纳 JSONL 后端已记录的理由:后续 process.cwd() 变化不得把一个后端劈到多个根下。SQLite 存储后端已经 resolve 其路径。
  • 适用 pre-release 立场:不做迁移垫片。曾在 <cwd>/.storages 下缓存过的部署要么全部重新派生(工作区从 header 索引重新 bootstrap投影缓存惰性重折要么手动把两个 json 文件挪一次。

声明派生介质:损坏时重置而非拒绝

  • DomainSpec 增加 recovery?: 'reject' | 'reset'(默认 'reject'。spec 对象已经是一个域的身份与布局的单一来源;其介质是权威还是派生属于同类事实,落在同一处。session_projcache 声明 'reset'workspace 保持默认。
  • KvFacet 增加一个原语:destroy(descriptor): Promise<void>——整体移除该 unit 的介质json删文件sqlitedrop 该 unit 的表)。与 open 一样,它是后端存储原语,不是策略。
  • DomainFacility.open 在 spec 声明 'reset' 且 open 恰以损坏类错误失败时——StorageError('version-mismatch' | 'malformed-medium')DomainError('invalid-record')——记一条命名该域和被丢弃介质的警告,调用 destroy,再空开一次。其余一切失败(backend-not-foundfacet-unsupportedalready-open、I/O 错误)无论声明与否都保持大声:配置错误和环境故障不是介质损坏。重试单发——第二次失败原样传播,持续失败的介质不会成环。
  • 有了这个,缓存域 spec 的 version 字段才获得其本意bump version(或让 zod 拒绝漂移行)真正丢弃整个介质,缓存经正常写点和冷读重建——恢复阶梯的最外一档,与已落地的行级各档对齐。

Alternatives considered

保持按启动目录的 .storages(现状)——拒绝:会话是全局的,所以每个从会话派生的介质都与自己的真源劈叉;缓存的动机场景(一次列出全部会话)结构性丢行,工作区注册表索引着从另一个启动目录看不见的会话。

只把投影缓存的 route 指到全局根,workspace.json 留在 per-cwd——拒绝:工作区注册表有一模一样的全局 vs per-cwd 错位,而且塑造缓存的用户决策就是刻意把它放在 workspace.json 旁边——一个 hub 根让介质同址、心智模型单一。

缓存插件本地恢复(在 SessionProjectionCache[Service.init] 捕获损坏错误、删文件、重开)——拒绝:插件不越过后端抽象就叫不出介质路径,且未来每个派生域都要重抄同一段 catchfacility 是唯一已经在分类 open 失败的地方。

损坏时退到内存态临时域——拒绝:进程余生静默降级为仅内存,损坏文件永不自愈;下次启动照样失败。

把损坏介质改名旁置(<unit>.json.corrupt-<ts>)而非删除——未选:派生介质的损坏字节没有恢复价值(日志才是真源),残骸无界累积;删除才是诚实的操作。若未来某个权威域想要重置语义,旁置改名才是对的——这正是 recovery 按 spec 声明的理由。

所有域一律自动重置(不加 spec 字段)——断然拒绝:workspace.json 是权威用户数据;版本 bump 时静默重置会毁掉工作区。权威性是域的属性,必须由其所有者声明。

Acceptance criteria

  • 从任意目录启动 dsh 都读写同一份 $DSH_HOME/storages/*.json(默认 ~/.dsh/storagesprofile 键 storageRoot 可覆盖;裸 Loader 启动 yml 仍落在相对启动 cwd 的 ./.storages,并在后端构造时 resolve 一次。
  • session_projcache.json 被截断、版本 bump 或 schema 漂移时,组装干净启动:一条警告命名被丢弃的介质,文件消失,缓存经正常运转重建,冷列表列随会话重新 checkpoint 逐步回归。
  • 同样的损坏发生在 workspace.json 上仍大声拒绝启动。
  • facility 测试覆盖:每个损坏类恰好重置一次 'reset' 域;非损坏失败在 'reset' 域上保持大声;'reject' 域传播一切失败;destroy 在两个出厂后端上都移除介质。

Risks

  • 错误分类失误导致自动删除健康文件。 由封闭的损坏类清单缓解重置只在三个确定性解析期代码上触发ENOENT 本来就是「空 unit」,一切 I/O 错误EACCES、EIO大声传播。单发重试把爆炸半径限定为每次 open 至多一删。
  • 根迁移改变既有 checkout 的查找位置。 在 pre-release 立场下接受(后端拒绝旧格式、无外部消费者);上文为在乎 per-cwd workspace.json 内容的人记录了一次性手动搬移。
  • destroy 是存储 seam 上新增的破坏性原语。 唯一调用方是 facility 的声明重置路径;后端契约将其记档为 facility 专属,任何面向模型或面向用户的路径都触不到它。