JianFeeeee
9985c41b13
fix(dsh-bridge): 新开会话登记工作区,workspaceOf 加淘汰兜底
read_inbox 在**新开会话**里恒 400("缺少 workspace"),而 read_mail /
read_thread / list_contacts / session_participants 都不受影响。
## 根因
`workspaceOf()` 取不到工作区就返空,URL 不带 `&workspace=`,服务端 400
(`server/internal/handler/mail.go:626`,用户裁定:不带 workspace 是错误
发件格式)。而 `sessionWorkspace` 只有两个写入点 —— adopt(bindAdopted)
与确定性 id 恢复 —— **新开会话分支漏了**。
漏了之后只有两种情况能发现:那条会话后来恰好走过另外两条绑定路径,
或者重启后 `sessionWorkspace`(500 条上限)把它淘汰掉。
## 改法
1. 新开会话分支补 `sessionWorkspace.set(attemptSessionId, sessionCwd)`。
★ 取值必须是 `header.cwd`,**不是那个 `cwd`**:后者来自
`resolveWorkspaceCwd`,`to_workspace` 不可用时回退到
`mailSessionFallback` → `~/.dsh/mail-sessions/mail-<uuid>`,每次邮件
都不同。拿它登记会把收件箱收窄到一个**永远读不到信**的目录 ——
比 400 更坏,因为 400 至少是可见的错误。同一文件下方工作区注册那段
(`actualCwd`)也是从 header 取,两处保持同源。
2. `workspaceOf()` 读侧兜底:反查 `sessionMap` 的 `directory`。
补在读侧而不是写侧,因为淘汰是**事后**发生的:补写站点只能覆盖
「我这次走过」,被淘汰的键下次谁来读都读不到。反查的 `directory`
在两条绑定路径上都是真实 cwd(adopt 走 `persistedCwd` 读磁盘
header),不是兜底目录。
## 影响面
`read_inbox` 的两维收窄(session_id + workspace)是**防越界读**的机制
(2026-09-14 用户报的越界)。这个改动让更多会话能拿到 workspace,因此
**可见范围确实变宽**了 —— 这是有意的(原本是读不到,不是读得少),
但复核时应当盯住这一条。
## 测试
新增 test/workspace-of-fallback.test.mjs(7 项)。其中 5 项把
`workspaceOf` 的真函数体抠出来在真 BoundedMap 上跑(不复制一份实现,
否则源文件改了测试还在绿)。已做变异验证:删掉读侧兜底 → 3 项红;
删掉写入点 → 2 项红。
dsh 桥 414 项全绿(改前 407,无回归)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>