Files
memind/docs/architecture/memind-2-streaming-agent-runtime-plan.md
T
2026-07-02 07:12:18 +08:00

20 KiB
Raw Blame History

Memind 2.0 Streaming Agent Runtime 改造计划

更新时间: 2026-07-02

执行状态:

  • P0 StreamController v1: 已完成代码落地、语法检查、Portal kickstart 和基础健康验证。
  • P1 Gateway SSE 配置: 已完成,mm.tkmind.cn 接入 PortalSSE route 已禁缓冲。
  • P2 Redis Runtime State + Router v1: 已完成可回退版本并启用 Redis。
  • P3 Observability: 已完成第一版,只读 runtime 状态接口已上线。
  • P4 Tool Gateway v1: 已完成第一步,普通用户默认不再暴露 Aider/OpenHands,显式用户白名单保留。
  • P5 Worker Pool 运维化: 已完成第一步,Redis Router 支持 worker drain。
  • 生产同步分支: 已从远程 origin/main 新建干净副本和分支 memind-streaming-runtime-20260702,用于远程开发机后续直接拉取。
  • P3.5/P5.5 生产化补强: 进行中,先完成生产数据库和 MindSpace 备份,再增加 runtime metrics、健康检查脚本和 drain 运维脚本。

目标

把当前 Memind H5 + multi-goosed 架构升级为 streaming-first agent runtime:

  • 易扩展: goosed worker pool 横向扩展,调度层按压力分配新会话。
  • 低延迟: SSE 首 token 优先,代理层禁缓冲、及时 flush、处理 backpressure。
  • 高稳定: 流式链路、工具执行、worker 状态、持久化状态分层隔离。

当前基线

  • Memind Portal: /Users/john/Project/Memind/server.mjs,本机 :8081
  • H5 public base: https://mm.tkmind.cnm.tkmind.cn 暂时不再作为 H5 public base。
  • goosed worker pool: Docker/Colima 内 goosed-prod-1..4,宿主机 18006..18009,当前健康。
  • H5 上游配置: TKMIND_API_TARGETS=https://127.0.0.1:18006,...,18009
  • 当前已有 session affinity: start 时分配 worker,后续 reply/events 回到同一 worker。
  • imgproxy: 原生 127.0.0.1:20082,兼容代理 10.10.0.2:20081
  • 风险点: SSE 代理尚未完整处理 flushHeadersX-Accel-Buffering、客户端断开 abort、写入 backpressure 和统一 pipeline 收尾。

目标架构

flowchart LR
  U["H5 / WeChat / Web"] --> E["Edge Gateway (Nginx / Envoy)"]
  E --> S["Stream Controller (Memind Portal v2)"]
  S --> R["Router / Scheduler"]
  R <--> Redis["Redis Runtime State"]
  S <--> Redis
  R --> G1["goosed-prod-1"]
  R --> G2["goosed-prod-2"]
  R --> G3["goosed-prod-3"]
  R --> G4["goosed-prod-4"]
  G1 --> T["Tool Gateway"]
  G2 --> T
  G3 --> T
  G4 --> T
  T --> A["Aider Service"]
  T --> O["OpenHands Service"]
  S --> PG["PostgreSQL / MySQL Persistence"]

分层原则

  1. Control Plane: Redis + Router,负责 worker 心跳、active stream、错误率、首 token 延迟、session worker pointer。
  2. Stream Data Plane: Memind Portal StreamController + goosed worker,负责低延迟流式传输。
  3. Persistence: 现有数据库只保存最终状态、会话历史、计费、发布记录,不保存高频 runtime state。
  4. Worker Affinity: goosed 尽量业务无状态,但 session 运行期必须 worker-affine;已存在 session 优先回原 worker。
  5. Tool Isolation: Aider/OpenHands 从默认全开逐步改为按任务启用、队列化、限流、超时和熔断。

阶段计划

P0: StreamController v1 加固

目标: 不引入新基础设施,先把现有 H5 SSE 链路做稳。

任务:

  • /api/sessions/:sessionId/events 增加 SSE 专用响应头:
    • Content-Type: text/event-stream; charset=utf-8
    • Cache-Control: no-cache, no-transform
    • Connection: keep-alive
    • X-Accel-Buffering: no
  • 在响应开始后调用 flushHeaders()
  • 使用 AbortController 将客户端断开传递到 goosed upstream fetch。
  • 用 backpressure-aware writable sink 替代裸 res.write()
  • pipeline() 统一处理 source、sanitizer、billing、client sink 的关闭和错误。
  • 保留 keepalive 和 billing balance event。

验收:

  • node --check /Users/john/Project/Memind/server.mjs 通过。
  • 本机 http://127.0.0.1:8081/api/status 仍返回 ok
  • 四个 https://127.0.0.1:18006..18009/status 仍返回 ok

P1: Gateway SSE 配置

目标: mm.tkmind.cn 新入口和本机入口不缓冲 SSE;后续 H5 不再走 105 nginx 反向隧道转发。

任务:

  • /api/sessions/*/events/api/agent/runs/*/events 配置:
    • proxy_buffering off
    • proxy_cache off
    • gzip off
    • proxy_read_timeout 3600
    • proxy_send_timeout 3600
    • add_header X-Accel-Buffering no always
  • 确认 mm.tkmind.cn/api/status 返回真实 H5 API,而不是维护页 HTML。
  • m.tkmind.cn -> 105 nginx -> 127.0.0.1:19081 -> reverse SSH tunnel -> Portal :8081 标记为 legacy/rollback-only。

验收:

  • mm.tkmind.cn/api/status 返回后端 ok 或明确 JSON health,而不是维护页 HTML。
  • SSE 响应头包含 X-Accel-Buffering: no

P2: Redis Runtime State + Router v1

目标: 从 round-robin 升级为 pressure-aware routing。

Redis key:

worker:{id}:heartbeat
worker:{id}:active_streams
worker:{id}:active_sessions
worker:{id}:ewma_first_token_ms
worker:{id}:error_rate
worker:{id}:memory_pressure
worker:{id}:drain
session:{session_id}:worker
stream:{stream_id}:status
stream:{stream_id}:started_at

调度分数:

score =
  active_streams * 3
+ active_sessions * 1
+ ewma_first_token_ms * 0.01
+ error_rate * 5
+ memory_pressure * 2

原则:

  • 已存在 session 优先使用 session:{id}:worker
  • 新 session 选择最低 score 的健康 worker。
  • heartbeat 过期 worker 不接新 session。
  • 不做无损中途迁移,除非 goose 支持完整 session restore。

P3: Observability

目标: 调度不靠感觉。

指标:

  • stream open/close/abort count
  • active_streams gauge
  • first_token_latency_ms
  • stream_duration_ms
  • worker fetch error rate
  • billing finish count/error count

先写日志和内存聚合,后续接 Prometheus 或 Redis。

P4: Tool Gateway v1

目标: Aider/OpenHands 工具隔离。

任务:

  • 普通聊天默认不暴露 Aider/OpenHands。
  • 代码任务按策略启用 Aider 或 OpenHands。
  • Tool Gateway 提供 queue、timeout、retry、并发上限、失败熔断。
  • stream 中输出 tool progress event,避免用户界面无反馈。

第一阶段实际落地:

  • 普通用户默认不暴露 Aider/OpenHands。
  • 保留显式 user capability override,作为白名单。
  • 后续再将 Aider/OpenHands 从 goosed platform extension 进一步拆成 queue-based Tool Gateway。

P5: Worker Pool 运维化

目标: 多 goosed 的发布、回滚、扩缩容可控。

任务:

  • 固化 goosed-prod-1..N compose/启动配置。
  • 每个 worker 独立日志、健康检查、资源上限。
  • drain 模式: 不接新 session,等 active stream 清零后升级。
  • 保留一键回滚镜像 tag。

Drain 操作:

docker exec memind-runtime-redis redis-cli SET memind:runtime:worker:goosed-3:drain 1
curl -sk https://mm.tkmind.cn/api/runtime/status
docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:drain

说明:

  • drain=1 后 Router 不再给该 worker 分配新 session。
  • 已存在 session 仍保持 worker affinity,不做中途迁移。
  • activeStreams=0 后再重启或升级该 worker。

执行顺序

  1. P0 立即执行,低风险高收益。
  2. P1 基于 mm.tkmind.cn 新入口执行;除非明确回滚,不再改造 105 H5 转发链路。
  3. P2/P3 一起推进,先观测再调度。
  4. P4/P5 在流式链路稳定后推进。

执行记录

2026-07-02 P0 StreamController v1

变更文件:

  • /Users/john/Project/Memind/server.mjs

备份:

  • /Users/john/Project/Memind/server.mjs.bak-streamcontroller-20260702-0628

已完成:

  • proxySessionEvents 增加 upstream AbortController,客户端断开时主动 abort goosed SSE fetch。
  • SSE 响应头升级为 text/event-stream; charset=utf-8no-cache, no-transformX-Accel-Buffering: no
  • 响应开始后调用 flushHeaders()
  • Writable sink 处理 res.write() backpressure,并保留 billing balance event 注入。
  • pipeline(source, sanitizer, billing, sink) 统一管理 SSE 管道收尾。
  • 通过 launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal 让变更生效。

验证:

  • /opt/homebrew/opt/node@24/bin/node --check /Users/john/Project/Memind/server.mjs 通过。
  • Portal 新 PID: 89465,监听 *:8081
  • http://127.0.0.1:8081/api/status 返回 ok
  • https://127.0.0.1:18006/status 返回 ok
  • https://127.0.0.1:18007/status 返回 ok
  • https://127.0.0.1:18008/status 返回 ok
  • https://127.0.0.1:18009/status 返回 ok

2026-07-02 H5 public base 临时切换

决策:

  • H5 public base 从 https://m.tkmind.cn 临时切换为 https://mm.tkmind.cn
  • 后续 H5 公网访问不再走 105 转发链路。
  • 旧链路 m.tkmind.cn -> 105 nginx -> 127.0.0.1:19081 -> reverse SSH tunnel -> Portal :8081 仅保留为 legacy/rollback-only 说明,不作为 Memind 2.0 改造目标。

变更文件:

  • /Users/john/Project/Memind/.env
  • /Users/john/Project/Memind/scripts/run-memind-portal-prod.sh
  • /Users/john/Project/Memind/RUNBOOK.txt
  • /Users/john/Project/memind_architecture/memind-2-streaming-agent-runtime-plan.md

已执行:

  • H5_PUBLIC_BASE_URL 改为 https://mm.tkmind.cn
  • VITE_MINDSPACE_BASE 改为 https://mm.tkmind.cn
  • 将 Portal 启动脚本默认 H5_PUBLIC_BASE_URL 改为 https://mm.tkmind.cn
  • 更新 RUNBOOK.txt,明确 105 转发链路不再作为后续 H5 公网路径。
  • 通过 launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal 重启 Portal。

验证:

  • Portal 新 PID: 94324,监听 *:8081
  • 运行环境确认: H5_PUBLIC_BASE_URL=https://mm.tkmind.cnVITE_MINDSPACE_BASE=https://mm.tkmind.cn
  • 本机 http://127.0.0.1:8081/api/status 返回 ok
  • 四个 goosed worker 18006..18009 均返回 ok
  • 公网 https://mm.tkmind.cn/api/status 当前返回 nginx 502 Bad Gateway,说明域名侧入口尚未接到当前 Portal;该问题纳入 P1 新入口修复,不再回到 105 转发链路处理。

2026-07-02 P1 Gateway SSE 配置

变更文件:

  • /opt/homebrew/etc/nginx/servers/mm.tkmind.cn.conf
  • /Users/john/Project/Memind/scripts/wechat-mp-menu.mjs
  • /Users/john/Project/Memind/server.mjs
  • /Users/john/Project/Memind/dist/dev/wechat-share-demo.html
  • /Users/john/Project/Memind/public/dev/wechat-share-demo.html

备份:

  • /opt/homebrew/etc/nginx/backups/mm.tkmind.cn.conf.bak-streaming-20260702-0635

已完成:

  • mm.tkmind.cn 通过本机 nginx 反向代理到 Portal 127.0.0.1:8081
  • /api/sessions/<id>/events/api/agent/runs/<id>/events 增加 SSE 专用 nginx location:
    • proxy_buffering off
    • proxy_cache off
    • gzip off
    • proxy_read_timeout 3600s
    • proxy_send_timeout 3600s
    • add_header X-Accel-Buffering no always
  • 通用 location / 增加 proxy_read_timeout 300sproxy_send_timeout 300s
  • 微信菜单脚本默认入口改为 https://mm.tkmind.cnhttps://mm.tkmind.cn/space
  • server.mjsH5_PUBLIC_BASE_URL 缺省值改为 https://mm.tkmind.cn

验证:

  • nginx -t 通过。
  • nginx -s reload 已执行。
  • https://mm.tkmind.cn/api/status 返回 ok
  • 未登录访问 https://mm.tkmind.cn/api/sessions/test-session/events 返回 401,且响应头包含 x-accel-buffering: no
  • 未登录访问 https://mm.tkmind.cn/api/agent/runs/test-run/events 返回 401,且响应头包含 x-accel-buffering: no

2026-07-02 P2 Redis Runtime State + Router v1

变更文件:

  • /Users/john/Project/Memind/server.mjs
  • /Users/john/Project/Memind/.env

新增运行组件:

  • Docker 容器: memind-runtime-redis
  • 镜像: redis:7-alpine
  • 绑定: 127.0.0.1:6379->6379
  • 持久化卷: memind-runtime-redis-data
  • 启动参数: redis-server --appendonly yes
  • 重启策略: unless-stopped

已完成:

  • 新增可选 createRuntimeRouter
  • MEMIND_RUNTIME_REDIS_URL 未配置时自动回退到原 round-robin。
  • MEMIND_RUNTIME_REDIS_URL 配置后启用 Redis scheduler。
  • 新 session 选择最低 score 的健康 worker;同分时仍按轮转顺序打散,避免全部压到第一个 worker。
  • session pointer 写入 Redis:
    • memind:runtime:session:<id>:worker
    • memind:runtime:session:<id>:target
  • SSE 打开/关闭维护 Redis active stream:
    • memind:runtime:worker:<id>:active_streams
    • memind:runtime:worker:<id>:heartbeat
    • memind:runtime:stream:<id>:status

验证:

  • docker exec memind-runtime-redis redis-cli PING 返回 PONG
  • Redis AOF 已开启。
  • Portal 运行环境包含 MEMIND_RUNTIME_REDIS_URL=redis://127.0.0.1:6379/0
  • Portal 日志出现 [RuntimeRouter] Redis scheduler enabled
  • Redis 当前可见 key 示例:
    • memind:runtime:worker:goosed-3:active_streams=1
    • memind:runtime:worker:goosed-3:heartbeat=<timestamp>
    • memind:runtime:stream:20260701_24:status=active
  • https://mm.tkmind.cn/api/status 返回 ok
  • 四个 goosed worker 18006..18009 均返回 ok

2026-07-02 P3 Observability v1

变更文件:

  • /Users/john/Project/Memind/server.mjs

已完成:

  • 新增只读接口 GET /api/runtime/status
  • 接口返回:
    • publicBaseUrl
    • Redis Router 是否启用
    • Redis namespace
    • 每个 worker 的 targetactiveStreamsactiveSessionsewmaFirstTokenMserrorRatememoryPressureheartbeat
    • 每个 TKMIND_API_TARGETS 上游的 /status 健康状态
  • 该接口与 /api/status 一样作为运维健康读接口,不要求登录;不返回任何密钥。

验证:

  • /opt/homebrew/opt/node@24/bin/node --check /Users/john/Project/Memind/server.mjs 通过。
  • https://mm.tkmind.cn/api/runtime/status 返回 ok: true
  • 返回中 router.enabled=truenamespace 为 memind:runtime
  • 返回中四个目标 18006..18009healthy: true
  • https://mm.tkmind.cn/api/status 返回 ok
  • 未登录访问 https://mm.tkmind.cn/api/sessions/test-session/events 返回 401,且响应头包含 x-accel-buffering: no

2026-07-02 P4 Tool Gateway v1 第一阶段

已完成:

  • 只读验证发现数据库中 role=user 曾将 aideropenhands 覆盖为 allowed=1
  • 已将 h5_capability_grantssubject_type='role' AND subject_id='user'aideropenhands 改为 allowed=0
  • 保留两个显式 user override:
    • a6fb1e97-2b0f-447b-b138-4561d8e5c53e
    • a70ff537-8908-486e-9b6c-042e07cc25db

效果:

  • 普通用户新建/恢复 session 时,Portal policy 不再把 Aider/OpenHands 放入 extension_overrides
  • 已授权用户仍可继续使用。
  • goosed 全局 config 仍保留 Aider/OpenHands enabled,避免破坏显式授权用户和既有会话;后续 Tool Gateway 阶段再拆 queue/timeout/retry。

2026-07-02 P5 Worker Pool Drain v1

变更文件:

  • /Users/john/Project/Memind/server.mjs

已完成:

  • Redis Router scoring 支持读取 memind:runtime:worker:<id>:drain
  • 当 drain 值为 1 / true / yes 时,该 worker 对新 session 的 score 为不可选。
  • /api/runtime/status 返回每个 worker 的 drain 字段。

验证:

  • /opt/homebrew/opt/node@24/bin/node --check /Users/john/Project/Memind/server.mjs 通过。
  • https://mm.tkmind.cn/api/runtime/status 返回四个 worker healthy=true
  • https://mm.tkmind.cn/api/runtime/status 返回四个 worker drain=false
  • https://mm.tkmind.cn/api/status 返回 ok
  • 未登录访问 https://mm.tkmind.cn/api/sessions/test-session/events 返回 401,且响应头包含 x-accel-buffering: no

2026-07-02 生产同步分支

决策:

  • 当前是生产环境,改造过程中不随意删除数据、不清理持久化目录、不覆盖旧本地脏改动。
  • /Users/john/Project/Memind 是生产运行产物目录,不是 git 工作树。
  • 生产 release manifest 指向 git_head=4fc59729ee222734628dca001e172c127c84488f
  • 远端 https://git.tkmind.cn/tkmind/memind.gitorigin/main 当前正是该提交。

已执行:

  • 新建干净源码目录 /Users/john/Project/memind-clean-main-20260702
  • origin/main 创建分支 memind-streaming-runtime-20260702
  • 迁入本次已在生产验证的 StreamController、Redis Router、runtime status、mm.tkmind.cn 和 nginx SSE 配置样例。
  • 新增远程开发机同步说明 docs/architecture/remote-dev-sync.md

限制:

  • 不提交生产 .env、数据库、MindSpace/data/users/.tailscale/logs/、证书和密钥。
  • 只提交代码、配置模板、部署样例和架构/运维文档。

2026-07-02 P3.5/P5.5 生产数据保护与运维补强

生产数据保护:

  • 允许重启生产服务,但重启前必须保护好数据库和 /Users/john/Project/Memind/MindSpace
  • 备份目录: /Users/john/Project/memind_backups/20260702-065813-pre-p35-p55
  • MindSpace 使用 rsync -a 全量副本,文件数 2137,大小约 120M
  • 数据库为 Aliyun RDS MySQL goose,已导出:
    • mysql-schema.sql
    • mysql-manifest.json
    • mysql-jsonl/*.jsonl
    • 表数 85,行数 28296

改造内容:

  • Redis Router worker 状态增加:
    • stream_open_count
    • stream_abort_count
    • stream_error_count
    • last_stream_started_at
    • last_stream_ended_at
    • score
  • 新增只读检查脚本 scripts/check-stream-runtime.mjs
  • 新增 drain 运维脚本 scripts/runtime-worker-drain.mjs
  • runtime 构建模板同步上述脚本,并将 RUNBOOK 中主路径更新为 mm.tkmind.cn -> local nginx -> Portal :8081
  • 本地和远端分支提交 4420cca chore: add runtime observability ops
  • 修正检查脚本为 HEAD 探测 SSE 入口,避免健康检查打开真实上游 SSE;提交 c9b7252 fix: avoid opening sse in runtime check

2026-07-02 P4.5 Tool Gateway 过渡层第一阶段

目标:

  • 不拆 goosed 内部 Aider/OpenHands,不引入队列系统,先把普通聊天和代码工具任务隔离。
  • 普通聊天默认不注入 Aider/OpenHands,即使用户有显式白名单。
  • 只有显式 toolMode='code' 的代码任务 policy 才注入 Aider/OpenHands。

已完成:

  • buildAgentExtensionPolicy() 新增 toolMode,默认 chat
  • toolMode='chat' 时不注入 aider / openhands
  • toolMode='code' 时才注入白名单用户的 aider / openhands
  • Aider/OpenHands extension 增加:
    • timeout_ms
    • metadata.runtime_scope='code_tool_task'
  • userAuth.getAgentSessionPolicy(userId) 默认返回 chat policy。
  • 新增 userAuth.getCodeAgentSessionPolicy(userId) 作为后续代码任务入口。
  • /api/runtime/status 增加 toolRuntime 摘要:
    • defaultMode
    • codeToolMode
    • chatInjectsCodeTools
    • aiderTimeoutMs
    • openhandsTimeoutMs
  • 新增生产只读检查脚本 scripts/check-tool-runtime.mjs

生产验证:

  • scripts/check-tool-runtime.mjs 返回 ok=true
  • role=useraider / openhands 均为 false
  • 显式白名单用户数为 2
  • 白名单用户 chat mode 不含 Aider/OpenHandscode mode 具备 Aider/OpenHands。
  • scripts/check-stream-runtime.mjs 返回 ok=true,四个 worker healthy。
  • scripts/runtime-worker-drain.mjs status 显示四个 worker activeStreams=0

生产部署:

  • 允许重启生产 Portal,已使用 launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal 生效。
  • 本阶段不写数据库、不删除数据、不修改 /Users/john/Project/Memind/MindSpace
  • 生产代码备份: /Users/john/Project/memind_backups/20260702-071020-pre-p45-tool-guard

回滚策略

  • P0: 修改前保留 server.mjs 备份;如启动失败,恢复备份并 launchctl kickstart Portal。
  • P1: 修改 nginx 前备份 confnginx -t 成功后再 reload。
  • P2: Redis Router 默认可通过 env 开关退回当前 pickTarget() round-robin。
  • P4: Tool Gateway 默认关闭,通过用户策略逐步放量。