You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
与 README 已声明限制的关系:dsh-workspace/README.md:160-164 已声明「删除注册不删数据」
「re-adding a directory starts fresh — the old sessions do not come back automatically」
「归档单向」;本报告不主张改变这些产品决策,只主张:当记账被 3.2 静默回滚时,
用户会在什么都没做的情况下落到同一结局,而 3.3 让这个结局无法被发现、无法被修复。
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
报告类型:缺陷(Defect)
严重程度:高 —— 用户视角等同「会话被弄丢了」;会话日志完好,但 UI 中永久不可见,且无任何恢复入口
状态:本机数据已完整量化;发现一为确定性(单实例可复现),发现二为高置信度(结构证据充分,未端到端复现)
版本:
0.1.5-rc.2(@deepseek-ai/dsh、dsh-base、dsh-workspace、dsh-api-session-controller、dsh-session-persistence-jsonl、dsh-storage-json同版本)平台:macOS 15.6 arm64 / Node v22.23.2 / npm 全局安装 /
DSH_HOME=<HOME>/.dsh1. 现象
<DSH_HOME>/sessions/<cwd slug>/<session-id>/session.v3.jsonl.zstd,可正常解压、内容完整。<DSH_HOME>/storages/workspace.json的sessionIds不含这些会话;而WorkspaceEntity.sessionIds按 canonical cwd 过滤(
dsh-workspace/lib/index.js:102-103),未记账的会话在侧栏 / 会话列表 /搜索面板一律不可见。
proj-8):磁盘 2 条会话、记账 1 条。丢失的是session-ec9f3585…(2026-09-17T05:50:39.989Z 创建、11 轮、1.4 MB)。该工作区的注册记录createdAt = 2026-09-17T08:17:42.775Z,记录里唯一的会话session-6307b366…createdAt = 08:17:42.856Z:注册是这次新建会话时才出现的,旧会话被留在外面。2. 影响量化(本机实测)
workspace.json记账的会话workspace.json中的工作区注册按目录样例(
proj-164 条、proj-219 条、proj-39 条、proj-48 条、proj-57 条、proj-67 条、proj-73 条、proj-81 条……),完整分布见附录 C。3. 根因
3.1 会话可以「无归属」地落盘(确定性,单实例可复现)
session.create仅当请求带workspaceId时才挂载到工作区:dsh-api-session-controller/lib/types/commands.js:110-117。dsh-api-session-controller/lib/types/types.d.ts:253-258(workspaceId?/cwd?/sessionId?)。process.cwd():dsh-api-session-controller/lib/index.js:2729→
this.commands = new SessionCommandController(ctx, this.agents, process.cwd());create里const cwd = workspace?.path ?? request.cwd ?? this.defaultCwd(commands.js:102)。@hyzyn/dsh-search的「新建会话」兜底就是const id = await sessions.create()(该插件自带注释:优先走 GUI 服务、其次点侧栏按钮、最后才自己建)。本机证据:
<DSH_HOME>/sessions/<host-cwd slug>/下有两条 0 KB 会话,cwd 恰好等于 web 宿主的process.cwd(),且没有任何工作区注册:3.2 已入账的记账会被第二个宿主静默回滚(高置信度)
存储根与 profile 无关:
dsh-base/cordis.patch.yml:151→root: !!js dshHomePath('storages')。全机只有一个
workspace.json(find <DSH_HOME> -name 'workspace.json*')。workspace域未声明 layout(dsh-workspace/lib/index.js:248-260),按dsh-storage-json/lib/index.js:575落到single布局 → 整个域(global + 全部 records)写在一个文件里,任何一次变更都重写全量。
写入协议是「内存快照 → 临时文件 →
rename()覆盖」(dsh-storage-json/lib/index.js:26-38),模块注释(
:8-16)写着:即没有锁、没有 revision 校验、没有冲突检测;
dsh-storage-domain/lib/index.js:316-320的「single-open per domain name」只覆盖单进程内。
本机同时运行两个宿主且共用
DSH_HOME(lsof证据见附录 A):profile
web→*:3080,profiletest→127.0.0.1:3082。于是:宿主 A 把
workspace.json读进内存 → 宿主 B 新增注册/挂载会话并写回 → A 之后任意一次 workspace变更用陈旧快照覆盖 → B 的记账静默消失(反之亦然)。会话日志走的是另一套「每会话独立文件 + 追加写 +
no-clobber」协议(
dsh-session-persistence-jsonl/lib/index.js:2858-2888),所以只有记账丢。dsh-storage-json/README.md:12只泛泛建议「写量大或高并发时选 SQLite」,没有任何地方提示「同一
DSH_HOME并发运行多个宿主会丢记账」,宿主也不做检测 / 拒绝 / 只读降级。3.3 损坏不可观测、不可恢复
if (!this.state.initialized) { ...bootstrap... }(
dsh-workspace/lib/index.js:336-353,实现自:595)。initialized一旦为true,之后每次启动只做
replaceHeaderIndex,永远不再收编「磁盘有日志但没有记账」的会话。create / rename / delete / insertBefore / insertSessionBefore / archiveSession / createDirectory / pick / list,没有 rescan/reconcile/rebuild;客户端
sessions.open(id)只能选中「已列出」的会话(dsh-api-session-controller/lib/client.js:3089-3095),搜索面板点开走的还是同一个
sessions.open()。ctx.logger.warn打到宿主 stdout(
dsh-workspace/lib/index.js:737-747),宿主是终端前台进程、日志不落盘。4. 最小复现
R1(无归属落盘,单宿主确定性)
Q没有工作区注册。session.create且不带workspaceId({cwd: Q},或什么都不带 → 落在宿主process.cwd())。workspace.json中没有任何记录包含它的 id。R2(双宿主覆盖)
DSH_HOME下同时启动 profile A(3080)与 profile B(3082)。P1并新建会话 → 出现P1注册。P2并新建会话 → 出现P2注册。5. 建议修复
P0-1 · 让「落盘的会话必有归属」成为宿主不变量
session.create在workspaceId缺省时按最终解析出的 cwd 做resolveByPath():命中则attachSession,未命中则创建注册(或至少明确报错)。另外
defaultCwd = process.cwd()(lib/index.js:2729)对 GUI 宿主是个尴尬落点,建议改为「无 cwd、无 workspaceId 的创建必须由连接上下文给出 cwd」。
P0-2 · 单写者保护 / 多实例检测
dsh-storage-json增加跨进程互斥(同目录锁文件 + PID/心跳 + 陈旧锁回收),或写入前比对mtime/revision,发现外部变更就重载合并、不可合并则报错,而不是静默覆盖;workspace域改成layout: "per-record"(session_projcache已是该布局),把覆盖粒度降到单工作区;DSH_HOME是否已有活跃宿主」,第二个实例拒绝启动或只读降级;至少在
dsh-storage-jsonREADME 中写明这一风险。P1 · 让记账可自愈 + 有入口
把一次性 bootstrap 改成可重入的 reconcile(启动时把未被任何注册收编的 header 按 canonical cwd 归并回去;
initialized不该是永久闸门),并提供dsh workspace reconcile(CLI)与 GUI「重新扫描历史会话」入口。P2 · 可观测性
reportFilteredCandidates()的结果进入可查询的诊断面,而不是只打 stdout;workspace.json写入带 revision + writer 标识,便于定位被回退的写入者。6. 我已做的临时缓解(仅供参考,不算修复)
workspace.json;global.initialized置回false,让 bootstrap 重新收编(本机 196/437 → 299/437;剩余 138 条属于完全没有注册的目录);
注册顺序与表一致)。
附录 A:双宿主证据(
lsof,节选,路径已脱敏)本机
profiles/test/cordis.patch.yml里为避开端口冲突固定了 3082,说明双开是有意的配置(不是偶发多开),因此这一并发形态很可能不是个例:
附录 B:核查脚本(只读,可直接运行)
本机输出(修复前):
附录 C:两种失效签名(判别线索)
对已有注册的工作区,比较「已记账会话的创建时间范围」与「未记账会话的创建时间范围」:
proj-1proj-3proj-4proj-10proj-7proj-5proj-11proj-6proj-9proj-8proj-9最干净:注册与其唯一会话在 09-14 07:03 同时建立,而同目录 07:30 之后创建的会话全都没被记账 ——说明「无归属落盘」可以发生在宿主正常使用、且工作区已注册的情况下。
附录 D:待确认的观察
无法确认是用户经 GUI 删除,还是 3.2 的覆盖 —— 这本身也说明缺少写入者标识/revision。
关于补丁:注意到本仓库外部 PR 已关闭(
has_pull_requests: false)。如果需要,我可以把「可重入 reconcile」或「
create时兜底 attach」的小改动放到 fork 分支上,再在这里贴链接;需要的话请告知倾向哪条修复路线。
说明:本报告只引用会话元数据(创建时间、字节数、按目录的计数)与代码位置,未包含任何会话消息内容、
业务文档、仓库名或凭据;路径与项目名已用占位符/别名替换。
All reactions