# 现象(生产实测)
在 DSH 界面上观察到的:每处理一封邮件就多出一条独立会话。
# 根因
`to_workspace` 是插件唯一能知道「这个任务该在哪个目录干活」的入口,而它取的是
**地址里的 path 位**。Agent 之间的回信、以及人在对话页点回复,地址里通常没有
path 位 —— 平台下发的 `reply_address` 就是这个形状(`FormatAddress(replyTo, "", alias)`)。
空着传下去的后果是可观测的:插件只能自己拼一个临时目录,于是**每封邮件落在一个
不同的空目录**里;DSH / opencode 按 cwd 给会话分组,界面上就成了「每处理一封邮件
就多出一条未分组会话」,而模型在那个空目录里什么项目文件也看不到。
实测取证:
- 线上 5 个兜底目录 `~/.dsh/mail-sessions/mail-*` **全部是空的**(0 条目)
- 全天 journalctl 里**没有任何**相关告警(代码用的是 `ctx.logger.warn`,
而同一文件别处明确写着 DSH 的 logger 不进 journalctl)→ 完全静默
- 走兜底的那条会话(8e982e96)里,`dsh → opencode` 那封 `to_workspace` 有值,
而 `opencode → dsh` 的回信 **to_workspace 全为空** —— 而该会话自身的
`sessions.workspace` 一直是有值的
# 修法
会话的 workspace 才是权威来源(见 models.SessionWorkspace 的注释):回信本来就是
回给**那条会话**的,而那条会话知道自己属于哪个项目。规则抽成纯函数
`resolveToWorkspace(addrPath, sessionWorkspace, toIsHuman)`:
1. 地址里写了 path → 照用(人的明确意图优先)
2. 没写且收件方是**人** → 保持空(人没有工作目录;填了前端会拼出
`gui-lab@/path.别名` 这种错地址,ToHuman 字段就是为此加的)
3. 没写且收件方是 Agent → 用会话的 workspace
**改的是 `to`,不是只改建库那一行**:同一个值还进投递载荷(`to_workspace` /
`self_address`)。改一处另一处不改,会出现「API 读到的与插件推到的不是同一个
目录」——那正是本项目一直在治的静默不一致。
`notify/mail.go` 只加了一段注释说明 reply_address 的 path 位为何**刻意留空**
(它的语义是「**发件人**该在哪儿干活」),免得后人以为那是漏填。
# 测试
- `handler/toworkspace_test.go`:6 条规则用例 + 1 条**反向对照**
(固定其他输入只翻转 toIsHuman,要求结果必须不同 —— 防止该参数被忽略后
「给人也填 path」静默回归)
- `repo/session_workspace_test.go`:锁住**列名与真实 schema**。这个查询读不到时
按设计返回空串,与「这条会话没有工作目录」无法区分 → 列名写错的功能表现是
「看起来还在跑,只是工作目录永远继承不到」
39 lines
1.9 KiB
Go
39 lines
1.9 KiB
Go
package handler
|
||
|
||
// resolveToWorkspace 决定一封邮件落库与投递时用的 `to_workspace`。
|
||
//
|
||
// # 它要解决的问题
|
||
//
|
||
// `to_workspace` 是插件唯一能知道「这个任务该在哪个目录干活」的入口。而 Agent
|
||
// 之间的回信、以及人在对话页里点回复,三维地址里通常**没有 path 位** —— 平台
|
||
// 下发的 `reply_address` 就是这个形状。空着传下去的后果是可观测的:
|
||
//
|
||
// - 插件只能自己拼一个临时目录,于是**每封邮件落在一个不同的空目录**里;
|
||
// - DSH / opencode 按 cwd 给会话分组,界面上就成了「每处理一封邮件就多出
|
||
// 一条未分组会话」;
|
||
// - 模型在那个空目录里什么项目文件也看不到,却照样开工。
|
||
//
|
||
// 会话的 workspace 才是权威来源(见 models.SessionWorkspace 的注释):回信本来
|
||
// 就是回给**那条会话**的,而那条会话知道自己属于哪个项目。
|
||
//
|
||
// # 三条规则,顺序不能换
|
||
//
|
||
// 1. 地址里写了 path → **照用**。那是人的明确意图,会话的 workspace 只是兜底。
|
||
// 2. 没写、且收件方是**人** → 保持空。人只有名字,没有工作目录;
|
||
// 给人类也填 path,前端与 agent_discovery 会拼出 `gui-lab@/root/proj.别名`
|
||
// 这种错地址(`ToHuman` 字段就是为避开这件事加的)。
|
||
// 3. 没写、且收件方是 **Agent** → 用会话的 workspace(可能仍为空串,
|
||
// 那表示这条会话确实没有工作目录,此时行为与从前一致)。
|
||
//
|
||
// 抽成纯函数是为了能直接对规则本身做判断:上面第 2 条是**反向对照**,
|
||
// 少了它,把「一律填 path」写错成看起来也合理。
|
||
func resolveToWorkspace(addrPath, sessionWorkspace string, toIsHuman bool) string {
|
||
if addrPath != "" {
|
||
return addrPath
|
||
}
|
||
if toIsHuman {
|
||
return ""
|
||
}
|
||
return sessionWorkspace
|
||
}
|