- 新增 web 能力并挂载 platform/web(web_search/fetch_url) - 实时查询强制 web skill,router fallback 与 await session Finish - Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
31 KiB
H5 Session 架构改造与开发设计方案
日期: 2026-07-06
状态: Phase 1 已完成;Phase 2 Patch 3/4a/4b/4c 已落地(分支 0706-bug干净,待全量 soak 后合并)。
旧路径 h5-session-gouzao0706.md 保留重定向至本文件。
适用范围:
- H5 前端聊天主链路。
- Memind Portal 内的 router、agent-run-gateway、tkmind-proxy、session ownership、SSE stream。
- goosed 作为 agent runtime 的接入边界。
非目标:
- 不重写 H5 UI。
- 不重写 goosed。
- 不引入新的大型框架或新平台。
- 不一次性重做完整 replay/event sourcing。
- 不改变当前生产发布规则和回归守卫。
- 不重复实施 Memind 2.0 Streaming Agent Runtime 计划 中已落地的 worker/queue/SLO 能力。
0. 结论
本次改造的本质不是重构系统,而是把当前 H5 + Portal + goosed 链路从:
socket/runtime 牵引的会话执行链路
收敛为:
Portal control plane + goosed execution runtime
最重要的架构转折点:
session 不再 socket 化。
Session 必须由 Portal 侧的 store/broker 管理,WebSocket/SSE/HTTP 都只能是 transport,不能成为 session state 的事实源。
当前系统已经具备较好的基础:
- H5 已经主要通过
POST /agent/runs提交聊天/agent 请求。 - 旧的
POST /sessions/:id/reply已经被拦截为410 AGENT_RUNS_REQUIRED。 h5_user_sessions已经保存 session ownership、origin、goosed_node、goosed_target。h5_agent_runs和h5_agent_run_events已经承担 run lifecycle 和基础事件记录。- H5 session event stream 已经走 SSE,并且前端已有
Last-Event-ID传递。
因此第一阶段不需要“建一个新系统”,而是抽一层很薄的 Session Broker,把已有能力从 user-auth.mjs、tkmind-proxy.mjs、server.mjs、agent-run-gateway.mjs 及若干旁路入口的散落调用中收口。
0.1 与相关文档的关系
| 文档 | 关系 |
|---|---|
| Memind 2.0 Streaming Agent Runtime 计划 | 已落地的 streaming/runtime、worker queue、SLO、tool gateway 等;本文在其之上做 session 控制面收口,不重复 P6–P8 内容 |
| Memind / MindSpace / Goose 解耦推进方案 | 更广的 Control/Execution + MindSpace 服务化;本文聚焦 H5 session ownership,不涉及 MindSpace 独立部署 |
| MindSpace 发布与聊天 Finish 回归守卫 | Patch 4 及 stream finish 相关改动必须遵守 |
| 103 Runtime Topology | 生产 goosed target 拓扑参考;broker 的 goosed_target pinning 与之对齐 |
1. 当前结构与主要问题
1.1 当前实际链路
flowchart LR
H5["H5 useTKMindChat"] --> API["src/api/client.ts"]
API --> RUN["POST /agent/runs"]
RUN --> GW["agent-run-gateway.mjs"]
GW --> ROUTER["chat-intent-router.mjs"]
GW --> DIRECT["direct-chat-service.mjs"]
GW --> PROXY["tkmind-proxy.mjs"]
PROXY --> GOOSE["goosed runtime"]
H5 --> RUNSSE["GET /agent/runs/:runId/events"]
H5 --> SESSSE["GET /sessions/:sessionId/events"]
RUNSSE --> GW
SESSSE --> PROXY
PROXY --> GOOSE
这个链路比早期状态更健康: H5 已经不是直接把普通聊天发到 goosed reply path。
但仍有三个核心问题:
- session lifecycle、ownership、target mapping 仍散落在多个模块里(见 §5.6 调用点清单)。
- gateway 同时处理 run lifecycle、router、direct chat、tool gateway、goosed session 创建,边界偏厚。
- SSE 已经存在,但事件语义还没有统一成稳定协议,尤其是 run stream 与 session stream 的终态/恢复语义不完全一致(见 §1.3)。
1.2 当前已有资产
当前不应重复建设的能力:
user-auth.mjsregisterAgentSession(userId, agentSessionId, goosedTarget)getSessionTarget(agentSessionId)ownsSession(userId, agentSessionId)unregisterAgentSession(userId, agentSessionId)
db.mjs- 自动补齐
h5_user_sessions.goosed_node - 自动补齐
h5_user_sessions.goosed_target - 自动补齐
h5_user_sessions.origin - 注意:
schema.sql中h5_user_sessions基表可能滞后于 runtime migration;以db.mjs启动迁移为准,Patch 1 实施时建议同步更新schema.sql注释或列定义。
- 自动补齐
tkmind-proxy.mjs- goosed target 选择与 session target 解析。
/sessions/:id/eventsSSE proxy。- stream started/ended metrics。
reconcileAgentSession调用(resume 路径)。
session-snapshot相关(sessionSnapshotService)- direct chat / portal snapshot 缓存。
GET /sessions/:id在 hint 匹配时的 DB snapshot 快路径。- gateway 执行前 transcript persist。
- 不是 ownership 事实源;与 broker 并列存在(见 §5.6)。
agent-run-gateway.mjsh5_agent_runsrow。h5_agent_run_events基础事件。- queued/running/retryable/succeeded/failed 状态。
- worker heartbeat 和 stale run recovery。
这些能力应该被收口和命名,不应被复制。
1.3 H5 双 SSE 通道现状
H5(useTKMindChat.ts + src/api/client.ts)在 agent 路径上同时订阅两条 SSE/事件通道:
| 通道 | 端点 | 职责 | 终态信号 |
|---|---|---|---|
| Run stream | GET /agent/runs/:runId/events |
run 排队/执行状态、sessionId 回填、direct chat 触发 snapshot 轮询 |
h5_agent_runs.status → succeeded / failed |
| Session stream | GET /sessions/:sessionId/events |
assistant token、tool call、finish 等 goosed 运行时事件 | goosed finish / error 等 terminal event |
典型时序:
POST /agent/runs
→ subscribeAgentRunEvents(runId) # 等待 run 终态、拿 sessionId
→ subscribeSessionEvents(sessionId) # 接收流式内容(非 direct chat)
→ finish 后 syncSessionMessages # merge,禁止盲覆盖
这解释了 §1.1 第三点: run 终态与 session 终态不是同一事件,direct chat 甚至主要依赖 run 成功 + snapshot poll,而非 session SSE。
Patch 4 必须明确:
- run stream: 保持现有 DB 驱动 polling/SSE,第一阶段不做 replay。
- session stream: 固化 delta/control/terminal 语义、
Last-Event-ID、finish 后 idle/sync。 - 禁止 在 Patch 4 中合并两条通道或改变 H5 订阅顺序。
2. 目标架构
flowchart LR
H5["H5"] --> PORTAL["Portal Control Plane"]
PORTAL --> ROUTER["Router\n decision only"]
PORTAL --> BROKER["Session Broker\n ownership + mapping"]
PORTAL --> GATEWAY["Agent Gateway\n run lifecycle"]
PORTAL --> STREAM["SSE Stream Module\n logical, in tkmind-proxy"]
PORTAL --> SNAP["Session Snapshot\n conversation cache"]
GATEWAY --> PROXY["goosed Proxy Adapter"]
STREAM --> PROXY
PROXY --> GOOSE["goosed\n execution runtime"]
BROKER --> DB["MySQL\n h5_user_sessions"]
GATEWAY --> RUNDB["MySQL\n h5_agent_runs/events"]
SNAP --> SNAPSTORE["Redis / in-memory\n not ownership"]
说明:
- SSE Stream Module 是逻辑模块,当前与
tkmind-proxy.mjs、server.mjs路由同进程部署,不是独立服务。 - Session Snapshot 缓存 conversation,与 Broker 分工不同,不可被 broker 吸收。
边界原则:
| 层 | 可以做 | 禁止做 |
|---|---|---|
| Router | 判断 chat/agent、reuse/new、sse/ws |
执行工具、写 memory、创建 session |
| Session Broker | ownership、target mapping、origin/timestamps | message history、tool state、execution state、memory |
| Session Snapshot | conversation 缓存、merge 输入、direct chat snapshot | ownership、goosed target、计费 |
| Agent Gateway | run lifecycle、排队、heartbeat、调用 broker/proxy | 直接持有 session mapping、绕过 proxy 打 goosed |
| Proxy Adapter | goosed HTTP/SSE transport、headers、abort、backpressure、reconcileAgentSession |
用户业务决策、router 决策 |
| goosed | runtime session、tool execution、event emission | H5 用户归属、计费、memory policy、业务路由 |
3. 协议分层
3.1 主链路
H5 -> Portal
POST /agent/runs
H5 <- Portal
GET /agent/runs/:runId/events
GET /sessions/:sessionId/events
Portal -> goosed
POST /agent/start
POST /sessions/:sessionId/reply
GET /sessions/:sessionId/events
GET /sessions/:sessionId
说明:
- H5 不直接调用 goosed。
- H5 普通聊天不走 WebSocket。
- WebSocket 只允许用于真正双向交互场景,例如 terminal、远程 shell、实时协作,不作为 H5 chat/session state 的事实源。
- SSE 是 H5 chat/agent stream 的主通道。
3.2 SSE 事件分类
第一阶段只做协议语义固化,不做完整 event sourcing。
统一把事件归为三类:
| 类别 | 含义 | 例子 |
|---|---|---|
delta |
内容增量 | assistant token、message chunk |
control |
状态变化 | active request、tool call、tool confirmation、balance、heartbeat |
terminal |
结束态 | finish、failed、canceled、error |
注意:
h5_agent_runs.status第一阶段不改枚举,仍保持queued/running/retryable/succeeded/failed。streaming/tool_call/completed/canceled先作为 event 语义,不直接写入 DB status。Last-Event-ID在 session stream 上继续保留;run stream 的 replay 后续再做,不在第一刀完成。- v0 策略: taxonomy 先在 Portal 侧文档化 + 单测断言映射关系,不改变 goosed 原始 event name 的 wire format;H5 仍按现有 event type 解析,仅在注释/测试层标注 taxonomy。
3.3 SSE event type → taxonomy 映射(v0 参考)
以下为 Patch 4 测试与文档用的语义映射,不要求 goosed 改协议:
| 来源 | 典型 event / 信号 | taxonomy | 备注 |
|---|---|---|---|
| session SSE | message / content chunk | delta |
assistant token 流 |
| session SSE | tool call / confirmation / balance | control |
不改变 active request 终态 |
| session SSE | finish | terminal |
必须触发前端 idle + syncSessionMessages merge |
| session SSE | error | terminal |
headers sent 后走 SSE frame,不走 JSON |
| run SSE | status: running | control |
DB 驱动 |
| run SSE | status: succeeded / failed | terminal |
关闭 run 订阅;agent 路径再依赖 session finish |
4. Router 改造设计
4.1 目标
Router 从单纯 classifier 收敛为轻量 decision output,但不升级成复杂 decision engine。
4.2 输出结构
新增统一输出字段:
{
"route": "chat",
"mode": "sse",
"session_hint": "reuse",
"flags": []
}
字段定义:
| 字段 | 允许值 | 含义 |
|---|---|---|
route |
chat / agent |
走直接聊天还是 agent 执行 |
mode |
sse / ws |
事件传输协议建议;H5 chat 固定消费 sse |
session_hint |
reuse / new |
倾向复用当前 session 还是新建 |
flags |
string array | 受控扩展位,只放风险/能力标签,不放执行计划 |
兼容要求:
- 旧值
direct_chat等价于chat。 - 旧值
agent_orchestration等价于agent。 - 旧字段
suggestedSkill、agentBrief第一阶段可以继续保留给现有逻辑使用,但不能扩展成多步 plan。
Router 可以读取上下文和 memory summary 做判断,但禁止:
- 创建 session。
- 写 memory。
- 执行 tool。
- 选择具体 goosed target。
- 输出多步执行计划。
4.3 字段消费方与语义
| 字段 | 消费者 | 第一阶段行为 |
|---|---|---|
route |
agent-run-gateway.mjs |
与现有 direct_chat / agent_orchestration 等价映射 |
mode |
暂无 H5 消费 | H5 chat 始终 SSE;ws 仅预留给 terminal/shell 等非 chat 场景,router 对 H5 chat 默认输出 sse |
session_hint |
agent-run-gateway.mjs |
建议性 hint,不 override 硬规则(见下) |
flags |
gateway / 日志 | 能力标签,不触发执行 |
session_hint 与 gateway 硬规则(hint 不能 override):
session_id 为空 → 必须新建 session(无论 hint)
session_id 无效/无 ownership → 403 或新建(沿用现有 gateway 行为)
direct chat 升级 deep reasoning → 清空 sessionId,强制 goosed start(hint reuse 失效)
force_deep_reasoning flag → 同上
tool gateway / code run → 可能无 goosed session(agent_session_id NULL),broker 不适用
isDirectChatSessionId → 不走 goosed session stream
第一阶段 H5 不读取 router normalized 字段;仅 gateway 内部使用,便于后续扩展。
4.4 flags 约束
允许的 flags 示例:
memory_recall
selected_skill
force_deep_reasoning
code_task
long_running
不允许的 flags:
run_tool_x
write_file_y
use_worker_2
bill_as_x
inject_memory_y
5. Session Broker 设计
5.1 目标
Session Broker 是一个薄 facade,不是第二套 session store。
它只负责:
- session ownership。
- session origin。
- session 与 goosed target 的 mapping。
- session lifecycle timestamp。
- session 删除/解绑。
它不负责:
- message history。
- tool state。
- execution state。
- memory。
- SSE replay。
- billing。
- provider selection。
5.2 与 Session Snapshot、session-reconcile 的边界
| 模块 | 职责 | 与 Broker 关系 |
|---|---|---|
| Session Broker | h5_user_sessions ownership + target |
事实源 |
| Session Snapshot | conversation 缓存、direct chat snapshot | broker 不管内容;snapshot 不得替代 ownership 校验 |
| session-reconcile.mjs | goosed session resume 时 working_dir 等字段对齐 | 由 proxy adapter 在 transport 层调用;broker 不封装 reconcile 逻辑 |
回归约束: GET /sessions/:id snapshot 缓存仅在 hint_mc 且 hint_ua 均匹配时命中(见 regression guard)。
5.3 建议文件
session-broker.mjs
session-broker.test.mjs
5.4 API v0
export function createSessionBroker({ userAuth, tkmindProxy = null }) {
return {
async validateOwnership(userId, sessionId) {},
async registerSession({ userId, sessionId, target, origin = 'h5' }) {},
async unregisterSession({ userId, sessionId }) {},
async resolveSession(sessionId) {},
async resolveSessionTarget(sessionId) {},
};
}
第一阶段不建议让 broker 直接承接所有 goosed start 行为。更稳妥的顺序是:
- v0 只包现有
userAuthsession 函数。 tkmind-proxy.mjs内部改为通过 broker 记录/读取 target。agent-run-gateway.mjs再通过 broker 做 ownership/session resolution。- 旁路入口(§5.6)逐步改走 broker。
5.5 direct chat 兼容
当前 direct chat session 使用 h5direct_ 前缀,并通过:
registerAgentSession(userId, activeSessionId, 'h5-direct')
写入 session 表。
Broker 必须把 h5-direct 当成合法 target/origin 信息处理,不能强行要求所有 session 都对应 goosed URL。
5.6 WeChat 兼容
h5_user_sessions.origin 已有 h5/wechat 语义。Broker v0 必须保留 origin,不要把 WeChat dedicated session 当成普通 H5 session 清理或旋转。
5.7 Session ownership 调用点清单(Patch 2 必须覆盖)
以下模块当前直接调用 userAuth.registerAgentSession / ownsSession / getSessionTarget,Patch 2 需全部纳入 broker 收口计划:
| 模块 | 典型调用 | Patch 批次 |
|---|---|---|
tkmind-proxy.mjs |
register / getSessionTarget / ownsSession | 2a |
agent-run-routes.mjs |
ownsSession(POST /agent/runs) | 2a |
agent-run-gateway.mjs |
间接 via tkmindProxy start | 2b |
direct-chat-service.mjs |
register h5-direct |
2b |
server.mjs |
多处 ownsSession(sessions CRUD、snapshot、delete 等) | 2c |
wechat-mp.mjs |
registerAgentSession | 2c |
mindspace-page-edit-session.mjs |
ownsSession / registerAgentSession | 2c |
mindspace-agent-runner.mjs |
registerAgentSession | 2c |
mindspace-conversation-package-routes.mjs |
ownsSession | 2c |
Patch 2 完成定义(量化):
# 除 session-broker.mjs 外,不应再直接调用 userAuth.registerAgentSession
rg 'userAuth\.registerAgentSession' --glob '*.mjs' | rg -v 'session-broker\.mjs|user-auth\.mjs|\.test\.mjs'
# ownsSession 同理(测试 mock 除外)
rg 'userAuth\.ownsSession' --glob '*.mjs' | rg -v 'session-broker\.mjs|user-auth\.mjs|\.test\.mjs'
broker 未覆盖 tool gateway / code run 的 无 session 路径: 这类 run 的 agent_session_id 可为 NULL,不经过 broker。
6. Agent Gateway 改造设计
6.1 当前边界
当前 agent-run-gateway.mjs 实际承担:
- run 创建。
- request idempotency。
- router 调用。
- direct chat 分支。
- tool gateway 分支。
- goosed session 创建。
- submit reply。
- heartbeat。
- stale recovery。
因此第一阶段不能按理想终态一次性“去 orchestration 化”,否则改动会明显变大。
6.2 第一阶段只抽 session 相关逻辑
第一阶段改造目标:
- 保留 run queue、heartbeat、stale recovery。
- 保留 direct chat 和 tool gateway 分支。
- 把 session ownership、session target mapping、session register/unregister 的调用收口到 broker。
建议变化:
agent-run-gateway
before: direct userAuth/tkmindProxy session calls
after: sessionBroker + tkmindProxy adapter
不要在第一阶段做:
- 删除 direct chat 分支。
- 重写 retry policy。
- 重写 stale recovery。
- 重写 run status enum。
- 重写 SSE run event handler。
6.3 Gateway 最终边界
长期目标:
Gateway = run lifecycle
Broker = session lifecycle
Proxy = runtime transport
Router = decision only
但实施必须分阶段完成。
7. Proxy / goosed 边界设计
7.1 硬边界(H5 chat 主链路)
H5 chat / agent 主链路上,Portal 到 goosed 的调用必须通过 tkmind-proxy.mjs 或其拆分后的 proxy adapter。
禁止新增:
server.mjs -> direct fetch goosed # H5 chat 路径
agent-run-gateway.mjs -> direct fetch goosed
chat-intent-router.mjs -> direct fetch goosed
H5 -> direct goosed
7.2 已知例外(MindSpace 专用路径)
以下模块当前通过 apiTarget 直连 goosed,不在 H5 chat 主链路内。Patch 5 静态检查须显式 allowlist,避免误报:
| 模块 | 用途 | Patch 5 策略 |
|---|---|---|
mindspace-agent-runner.mjs |
MindSpace agent 任务 | 保留独立 adapter;长期可迁入 proxy,第一阶段 document + allowlist |
mindspace-page-edit-session.mjs |
页面编辑 session | 同上 |
scripts/* 运维脚本 |
本地调试/fix | 排除在 prod boundary check 外 |
原则: 新增 goosed 调用必须走 proxy;存量 MindSpace 路径记录在案,不阻塞 Patch 1–4。
7.3 goosed 保留能力
goosed 继续负责:
/agent/start/sessions/:sessionId/reply/sessions/:sessionId/events/sessions/:sessionId- tool execution
- runtime event emission
goosed 不新增负责:
- H5 user ownership。
- 余额/计费。
- router 结果。
- memory policy。
- session broker 事实源。
7.4 target mapping
当前 goosed_target 已经用于 session pinning。Broker 不应替代这个字段,而应封装它。
目标行为:
new session:
proxy pick target
goosed start returns session id
broker records session_id -> target
existing session:
broker resolves session_id -> target
proxy sends reply/events to same target
8. SSE 设计
8.1 已有能力
已有能力:
- H5 session events 使用 fetch stream。
- 前端维护
lastEventId并传Last-Event-ID。 - Portal proxy 透传
Last-Event-ID到 goosed。 - Portal proxy 已处理 keepalive、abort、backpressure、billing transform、finish 后 snapshot refresh。
这些不能被第一阶段改造破坏。
8.2 第一阶段目标
只做语义固化:
- 明确事件分类:
delta/control/terminal(见 §3.3 映射表)。 - 明确 session stream terminal event 必须关闭当前 active request,并走
syncSessionMessagesmerge(useTKMindChat.ts)。 - 明确 run stream terminal(succeeded/failed)关闭 run 订阅,不替代 session finish 同步。
- 错误统一为 SSE
event: error,不要在 header sent 后再走 JSON error。 GET /agent/runs/:runId/events保持现有兼容,不强行改成完整 replay。
8.3 后续目标
第二阶段再考虑:
- run event id。
- run event replay。
- session-level replay cursor。
- reconnect 后从 DB 补 terminal state。
9. Run State Model
9.1 现状
DB status 当前保持:
queued
running
retryable
succeeded
failed
这些状态已经被 gateway、worker、status API、测试和前端使用。
9.2 目标语义
逻辑状态可以扩展为:
created
queued
running
streaming
tool_call
completed
failed
canceled
但第一阶段不改 DB enum,只做映射:
| 逻辑状态 | 第一阶段承载方式 |
|---|---|
created |
run row insert 前后 |
queued |
h5_agent_runs.status = queued |
running |
h5_agent_runs.status = running |
streaming |
session SSE control event |
tool_call |
session SSE control event |
completed |
succeeded + terminal event |
failed |
failed + terminal/error event |
canceled |
后续新增,不在第一阶段强推 |
9.3 Tool gateway / code run 说明
tool gateway 与 code run 路径可能:
- 不创建 goosed session(
h5_agent_runs.agent_session_id为 NULL)。 - 由 external worker / Aider / OpenHands 执行。
这类 run 不经过 Session Broker 的 target mapping;gateway 仍负责 run lifecycle。避免在 broker 设计中假设“每个 run 必有 agent session”。
10. 5 个 Patch 的实施版本
Patch 1: Session Broker facade
目标:
- 新增
session-broker.mjs。 - 单测覆盖 ownership、target mapping、direct chat target、legacy node fallback。
- 不改变行为。
改动文件:
session-broker.mjs
session-broker.test.mjs
验收:
- 所有现有 session 读写仍通过同一张
h5_user_sessions。 - 不新增 session cache。
- 不新增 message/tool/memory 字段。
schema.sql与db.mjs列定义对齐(或文档注明以 migration 为准)。
Patch 2: Proxy、Gateway 与旁路入口接入 Broker
目标:
tkmind-proxy.mjs中 session register/resolve 通过 broker facade。agent-run-routes.mjs中 ownership 校验通过 broker facade。agent-run-gateway.mjs只在 session 创建/复用处接 broker,不动 direct/tool/retry。direct-chat-service.mjs、server.mjs、wechat-mp.mjs等 §5.7 清单模块改走 broker。
改动文件:
# 2a
tkmind-proxy.mjs
agent-run-routes.mjs
# 2b
agent-run-gateway.mjs
direct-chat-service.mjs
# 2c
server.mjs
wechat-mp.mjs
mindspace-page-edit-session.mjs
mindspace-agent-runner.mjs
mindspace-conversation-package-routes.mjs
验收:
POST /agent/runs行为不变。GET /sessions/:sessionId/events行为不变。- direct chat session 仍能创建和读取 snapshot。
- goosed target pinning 仍正确。
- §5.7 量化 grep 检查通过(测试 mock 除外)。
Feature flag(建议):
MEMIND_SESSION_BROKER_ENABLED=1 # 默认 off → on;off 时 fallback 到 userAuth 直调
Patch 3: Router 输出收敛
目标:
- 在旧 router classification 上新增 normalized decision。
- 输出
route/mode/session_hint/flags。 - 旧字段继续兼容。
改动文件:
chat-intent-router.mjs
chat-intent-router.test.mjs
agent-run-gateway.mjs
验收:
- 旧 direct chat / agent orchestration 判断不变。
- Router 不执行 tool。
- Router 不创建 session。
- Router 不写 memory。
- H5 chat 场景
mode默认sse;session_hint不 override gateway 硬规则(§4.3)。
Feature flag(建议):
MEMIND_ROUTER_NORMALIZED_DECISION=1 # shadow: 只打日志不写行为;on: gateway 读取新字段
Patch 4: SSE event contract v0
目标:
- 文档化并测试 delta/control/terminal 分类(§3.3)。
- 保持现有 stream wire format 兼容。
- 修正 header-sent 后的错误输出,继续走 SSE error frame。
改动文件:
agent-run-routes.mjs
tkmind-proxy.mjs
src/api/client.ts # 仅注释/常量;尽量不改解析逻辑
src/hooks/useTKMindChat.ts # 若动 finish/sync 逻辑需跑 regression guard
相关测试
验收:
- session stream 断开重连不退化。
Last-Event-ID继续透传。- session terminal event 后前端能稳定 idle/sync(merge,非盲覆盖)。
- run stream terminal 与 session finish 职责分离(§1.3)。
- 不要求第一阶段完成完整 replay。
Patch 5: goosed 入口硬边界
目标:
- 明确 H5 chat 主链路上 Portal → goosed 的唯一入口是 proxy adapter。
- 增加测试或静态检查(
scripts/check-goosed-proxy-boundary.mjs或 eslint),防止新增绕过 proxy 的 goosed fetch。 - 对 §7.2 例外模块做 allowlist。
- 保留 goosed API,不改 goosed core。
改动文件:
tkmind-proxy.mjs
server.mjs
scripts/check-goosed-proxy-boundary.mjs # 新增
tests
验收:
- H5 不能直接访问 goosed。
- gateway/router 不直接 fetch goosed。
- 旧 reply path 继续返回
410 AGENT_RUNS_REQUIRED。 - MindSpace 例外模块在 allowlist 内,检查脚本通过。
11. 上线顺序
Phase 1: 安全收口
- Patch 1: Broker facade,无行为变化。
- Patch 2a: proxy + agent-run-routes 改走 broker,
MEMIND_SESSION_BROKER_ENABLED可回退。 - Patch 2b: gateway + direct-chat-service 接 broker。
- Patch 2c: server / wechat / mindspace 旁路入口接 broker。
Phase 2: 协议与 router 收敛
- Patch 3: router 输出新增字段,旧字段兼容;可先 shadow 日志。
- Patch 4a: SSE event taxonomy 和 terminal 行为测试。
Phase 3: 边界加固
- Patch 5: goosed proxy 唯一入口约束 + allowlist。
- Patch 4b: run stream replay / event id 评估,不在第一阶段强行上线。
12. 风险与控制
| 风险 | 等级 | 控制方式 |
|---|---|---|
| Broker 变成第二套 session store | 高 | 只包 h5_user_sessions,禁止 cache/message/tool/memory |
| Gateway 一次性重构过大 | 高 | 第一阶段只抽 session,保留 direct/tool/retry |
| Patch 2 漏改旁路入口 | 高 | §5.7 清单 + grep 量化验收 |
| Direct Chat 被误认为 goosed session | 中 | 保留 h5direct_ 和 h5-direct target |
| WeChat dedicated session 被误清理 | 中 | Broker 保留 origin,不统一 rotate |
| SSE 固化变成 event-sourcing 重构 | 高 | v0 只做 taxonomy/terminal,不改 wire format |
| Run/session 双通道语义混淆 | 高 | §1.3 明确分工;Patch 4 禁止合并通道 |
| goosed target list 扩缩导致 session 路由错乱 | 中 | 继续以 goosed_target 为优先,legacy node fallback |
| MindSpace Finish 同步被移动错层 | 高 | Finish 后 public HTML/materialize 逻辑仍留在 stream finish 处理附近 |
| 旧接口兼容破坏 | 中 | 保持 /agent/runs 返回结构、旧 reply path 410 |
| Snapshot 缓存误命中导致对话清空 | 高 | 保留 hint_mc + hint_ua 双 hint 规则 |
13. 验收清单
13.1 单测
建议新增/补充:
session-broker.test.mjs
chat-intent-router.test.mjs
agent-run-routes.test.mjs
tkmind-proxy.test.mjs
direct-chat-service.test.mjs
重点断言:
- ownership 校验失败返回 403。
- direct chat target
h5-direct可解析且不会走 goosed URL。 goosed_target优先于 legacygoosed_node。- router 新字段默认值稳定;
session_hint不 override 空 session_id。 /sessions/:id/reply仍返回 410。- session event error 在 headers sent 后走 SSE frame。
- run terminal 与 session finish 职责分离(§1.3)。
13.2 回归守卫
如果修改触及以下路径,必须执行对应 guard:
npm run verify:mindspace-publish-guards
npm run verify:mindspace-publish-guards:full
npm run verify:mindspace-page-sync-guards
npm run verify:chat-finish-sync # Patch 4 触及 useTKMindChat / client.ts 时必跑
受保护路径包括:
src/hooks/useTKMindChat.tschat-finish-sync.mjsmindspace-public-finish-sync.mjsconversation-display.mjsserver.mjstkmind-proxy.mjs
13.3 发布前验证
发布前必须:
bash scripts/check-release-ready.sh
并按当前仓库发布规则执行完整 main 打包发布,不允许从脏工作区或单修复散包发布。
13.4 可观测性(建议,非阻塞第一阶段)
broker/proxy 接入后可补充 metrics 或 structured log:
session_broker.resolve_target.miss # goosed_target 空且 node fallback
session_broker.ownership.denied # 403 计数
session_broker.register.duplicate # 重复 register
goosed_proxy.stream.started / ended # 已有,保持
用于验证「session 不再 socket 化」后排障是否按 broker → proxy → goosed 分层定位。
14. 回滚策略
Patch 1 回滚
删除 broker facade 和测试即可,无数据迁移。
Patch 2 回滚
设置 MEMIND_SESSION_BROKER_ENABLED=0,调用切回 userAuth 原函数。底层仍是同一张表,不需要 DB 回滚。
Patch 3 回滚
设置 MEMIND_ROUTER_NORMALIZED_DECISION=0,继续使用旧 direct_chat/agent_orchestration。
Patch 4 回滚
保留现有 SSE wire format;如果新 event taxonomy 影响前端,先关闭新解析逻辑,仍按旧 event type 处理。
Patch 5 回滚
恢复旧 proxy 调用路径或禁用 boundary check script,但必须保留旧 reply path 410,避免重新打开已修复的直接 reply 风险。
15. 实施禁区
第一阶段明确禁止:
- 新建第二套 session 表。
- 把 message history 放进 broker。
- 把 tool state 放进 broker。
- 把 memory 选择逻辑放进 broker。
- 把 session snapshot 合并进 broker。
- 把 router 改成多步 planner。
- 把 gateway 的 direct chat/tool gateway/retry 一次性拆掉。
- 合并 run stream 与 session stream 为单通道。
- 修改 goosed core。
- 修改 H5 UI(Patch 4 尽量避免改
useTKMindChat.ts行为)。 - 修改生产拓扑或 goosed 容器部署方式。
16. 预期解决的问题
执行后能解决:
- session 不再依赖某条 socket/stream 连接。
- session ownership 和 runtime target 有统一入口。
- 多 goosed target 下 session pinning 更清晰。
- H5 / Portal / goosed 协议边界更稳定。
- router 不再向执行规划膨胀。
- gateway 的 run lifecycle 与 session lifecycle 逐步分离。
- 生产排障能按 router、broker、gateway、proxy、goosed、SSE 分层定位。
不会直接解决:
- 模型回答质量。
- 单次推理速度。
- 工具本身执行失败。
- 已污染老 session 的自动修复。
- 完整 run/session replay。
- memory 内容质量。
这些属于后续 P2/P3 稳定性增强,不应塞进第一阶段。
17. 最终判断标准
改造是否成功,看三点:
- session 不再依赖 WebSocket/SSE 连接状态。
- goosed 不再承担 H5 用户、计费、router、memory 的业务决策。
- router 不输出执行步骤,只输出受控 decision。
补充量化验收:
- §5.7 调用点清单全部经 broker,grep 检查通过。
- H5 双 SSE 通道行为与 Patch 前一致(含 direct chat、finish merge、Last-Event-ID)。
只要以上成立,系统就从 socket-driven agent system 进入 event-driven control-plane system 的正确轨道。