diff --git a/gateway/internal/repo/repo.go b/gateway/internal/repo/repo.go index 99d0fd6..3afa22b 100644 --- a/gateway/internal/repo/repo.go +++ b/gateway/internal/repo/repo.go @@ -985,16 +985,19 @@ func FindSessionByAddress(ctx context.Context, name, path, alias string) (uuid.U // SuggestPaths 给出某个收件方可用的工作目录候选(三段式补全的 path 位)。 // -// 两个来源并集,**历史优先**: +// 三个来源并集,**历史优先**: // // 1. mails.to_workspace 里真实投递过的目录(按最近使用倒序) -// 2. agents.workspaces 里注册时自报的目录 +// 2. agent_platform_sessions.workspace 里心跳上报的目录(镜像) +// 3. agents.workspaces 里注册时自报的目录 // -// 早先只看第 2 项,而它对两个官方插件**永远是空的** —— 契约(W-2) -// 明确要求 `workspaces: []`,因为工作目录由每封邮件的 to_workspace 决定而不是 -// 注册时固定。于是补全的第二段对 dsh / opencode 一直给空列表, -// 人得手敲路径,Agent 更只能猜 —— 生产上 dsh 猜了 `opencode@/home`, -// 解析通过、投递成功,但那不是 opencode 的工作目录。 +// 早先只看第 1、3 项,而第 1 项在清库后为空,第 3 项对两个官方插件 +// **永远是空的** —— 契约(W-2)明确要求 `workspaces: []`, +// 因为工作目录由每封邮件的 to_workspace 决定而不是注册时固定。 +// 于是补全的第二段在清库后对所有 Agent 都给空列表。 +// +// 加入第 2 项:镜像是心跳上报的平台会话快照,其中 workspace 字段 +// 就是各平台的真实工作目录。它在清库后仍然存在(心跳重新上报)。 // // 按最近使用倒序而非字典序:同一个 Agent 常在几个项目间切,刚用过的那个 // 几乎总是下一封想用的那个。 @@ -1028,7 +1031,32 @@ func SuggestPaths(ctx context.Context, agentName string) ([]string, error) { } } - // 来源 2:注册时自报。报了但还没收过信的目录靠这一步进入候选, + // 来源 2:平台会话镜像。心跳上报的平台会话快照,workspace 就是 + // 各平台的真实工作目录。清库后仍然存在(心跳重新上报)。 + // 没有这一步,清库后路径补全对所有 Agent 都空。 + mrows, err := db.DB.QueryContext(ctx, ` + SELECT workspace, COUNT(*) AS n + FROM agent_platform_sessions + WHERE agent_name = $1 AND COALESCE(workspace, '') <> '' + GROUP BY workspace + ORDER BY n DESC + `, agentName) + if err == nil { + defer mrows.Close() + for mrows.Next() { + var p string + var n int + if mrows.Scan(&p, &n) != nil { + continue + } + if !seen[p] { + seen[p] = true + out = append(out, p) + } + } + } + + // 来源 3:注册时自报。报了但还没收过信的目录靠这一步进入候选, // 否则新接入的 Agent 在第一封邮件之前仍然无路径可选。 var wsJSON []byte if err := db.DB.QueryRowContext(ctx,