问题与价值
新建 Web Session 并自动发送首条消息时,前端没有把后续操作绑定到创建结果。另一个标签页若在创建响应与快照刷新之间切换活动 Session,且 SSE 切换事件尚未送达,首条消息可以被自动投递到另一个 Session,甚至另一个工作区。
建议 P1:这影响执行目标的正确性。用户为仓库 A 写的操作指令可能交给仓库 B 的 Agent。当前证据确认了错误的 prompt 请求目标,没有执行真实工具,也没有观测到用户仓库损坏。
触发前提与实际复现
源码:ed9dbc1018f890fd54375f5371990ddfee8af5df。验证日期:2026-09-08。
- 标签页 A 尚未创建 Session,选中
/repo-A,发送首条消息。
- 创建 API 成功返回该请求的
commandId 和 A 的 sessionPath。
- 另一个标签页激活
/repo-B 的 Session B;A 的 SSE 暂时没有收到该事件。
- A 随后的 snapshot 请求返回当前活动 Session B。
- A 的发送动作继续,把原来的消息交给 B。
直接加载生产 createWebStore、以内存 client 控制响应时序,得到:
createSession(path=/repo-A)
prompt(sessionId=sB, content="Edit repository A configuration", retry=false)
sendPrompt result=true
selectedWorkspace=/repo-B
selectedSession=sB
源码原因与现有保护
现有 Session epoch 可以在外部 SSE 切换事件先到时中止旧操作;本次覆盖的是 HTTP 已观察到新 Session、SSE 尚未到达的顺序。两条独立连接不能依赖这种先后关系保证 target 正确。
Host 检查和 runtime 检查都要求请求里的 Session ID 是当前活动 Session。这些检查没有失效:前端已经把用户原定目标换成合法的 B ID,所以它们无法识别原来的 A 意图。
探针关键片段
base 是没有选中 Session、包含 A/B 两个工作区的快照;activeB 是 B 当前活动的快照。测试直接使用生产 store,没有复制其状态逻辑:
let created = false;
const client = {
snapshot: async () => created ? activeB : base,
createSession: async (path, commandId) => {
created = true;
return { cancelled: false, sessionPath: "/web-sessions/sA.jsonl", commandId };
},
prompt: async (sessionId, content, commandId) => {
calls.push({ sessionId, content });
return { id: commandId, accepted: true };
},
};
const store = createWebStore(client);
await store.getState().actions.refreshSnapshot();
store.setState({ selectedWorkspace: "/repo-A" });
await store.getState().actions.sendPrompt("Edit repository A configuration");
最小验收
验证边界:本次是生产 store 的内存响应探针及服务端源码核实,使用 Bun 和已有 Zustand 依赖;未启动 Host/Pi、调用模型、访问真实 Session 或修改仓库文件。Related: #347、#455;不同于 #457 草稿归属和 #463 接收回执恢复。
问题与价值
新建 Web Session 并自动发送首条消息时,前端没有把后续操作绑定到创建结果。另一个标签页若在创建响应与快照刷新之间切换活动 Session,且 SSE 切换事件尚未送达,首条消息可以被自动投递到另一个 Session,甚至另一个工作区。
建议 P1:这影响执行目标的正确性。用户为仓库 A 写的操作指令可能交给仓库 B 的 Agent。当前证据确认了错误的 prompt 请求目标,没有执行真实工具,也没有观测到用户仓库损坏。
触发前提与实际复现
源码:
ed9dbc1018f890fd54375f5371990ddfee8af5df。验证日期:2026-09-08。/repo-A,发送首条消息。commandId和 A 的sessionPath。/repo-B的 Session B;A 的 SSE 暂时没有收到该事件。直接加载生产
createWebStore、以内存 client 控制响应时序,得到:源码原因与现有保护
commandId和可用时的sessionPath,但客户端没有使用;客户端返回类型还只声明了path。现有 Session epoch 可以在外部 SSE 切换事件先到时中止旧操作;本次覆盖的是 HTTP 已观察到新 Session、SSE 尚未到达的顺序。两条独立连接不能依赖这种先后关系保证 target 正确。
Host 检查和 runtime 检查都要求请求里的 Session ID 是当前活动 Session。这些检查没有失效:前端已经把用户原定目标换成合法的 B ID,所以它们无法识别原来的 A 意图。
探针关键片段
base是没有选中 Session、包含 A/B 两个工作区的快照;activeB是 B 当前活动的快照。测试直接使用生产 store,没有复制其状态逻辑:最小验收
bun run check、bun run test。验证边界:本次是生产 store 的内存响应探针及服务端源码核实,使用 Bun 和已有 Zustand 依赖;未启动 Host/Pi、调用模型、访问真实 Session 或修改仓库文件。Related: #347、#455;不同于 #457 草稿归属和 #463 接收回执恢复。