# Memind 2.0 Streaming Agent Runtime 改造计划 > 历史快照说明:本文记录 2026-07-02 streaming/runtime 改造过程,其中出现的 `/Users/john/Project/Memind/MindSpace` 是当时的存量数据保护/统计口径。103 当前生产 MindSpace Service 根目录以 [103 runtime topology](../103-runtime-topology.md) 为准,即 `/Users/john/MindSpace`;不要把本文历史路径当作当前服务根目录。 更新时间: 2026-07-02 执行状态: - P0 StreamController v1: 已完成代码落地、语法检查、Portal kickstart 和基础健康验证。 - P1 Gateway SSE 配置: 已完成,`mm.tkmind.cn` 接入 Portal,SSE route 已禁缓冲。 - P2 Redis Runtime State + Router v1: 已完成可回退版本并启用 Redis。 - P3 Observability: 已完成第一版,只读 runtime 状态接口已上线。 - P4 Tool Gateway v1: 已完成第一步,普通用户默认不再暴露 Aider/OpenHands,显式用户白名单保留。 - P6.0 Tool Gateway Queue v0: 已完成最小队列化运行时,`/agent/runs` 支持全局并发限制、运行超时和 runtime/SLO 可观测状态。 - P6.1 Tool Worker v1 骨架: 已完成外部 worker 接管入口,生产仍保持 Portal `autoDispatch=true`,未放量真实外置执行。 - P6.2 Tool Worker Canary: 已完成短窗口真实接管 canary,外部 worker 成功处理一条 chat-mode queued run,生产已恢复 Portal `autoDispatch=true`。 - P6.3 Tool Gateway 协议化: 已完成,已新增 `agent-run-v1` 协议模块,code run 可在显式开启后绕开 goosed session extension 交给 Aider/OpenHands 外部执行器。 - P6.4 Tool Gateway Real Canary: 已完成 Aider 真实执行 canary,修复 DeepSeek/Aider 模型名前缀问题后,专用空仓库产物生成成功。 - P6.5 OpenHands Real Canary: 已完成,OpenHands headless/json 在专用空仓库中生成目标产物,生产仍保持默认关闭。 - P6.6 External Worker Code-run Canary: 已完成,Portal autoDispatch 短窗口关闭,external `agent-run-worker --run-id` 精确接管一条 code run 并生成 canary 产物,生产已恢复默认关闭。 - P6.7 Tool Gateway Production Guardrails: 已完成第一步,Tool Gateway 支持 expected file/content 产物校验、stdout/stderr tail 审计和 validation failed 非重试失败语义,已发布生产且默认关闭。 - P6.8 Tool Gateway Controlled Rollout 基础: 已完成,external agent-run-worker LaunchAgent installer 已进入 runtime,生产已安装为 disabled/not running,后续可显式开启灰度。 - P6.9 Validated External Worker Canary: 已完成,短窗口启动 external worker,先验证 exit 0 但内容不符会 failed,再验证 expected content 命中会 succeeded;生产已恢复默认关闭,worker 已恢复 disabled/not running。 - P6.10 External Worker Observability: 已完成,只读 `check-agent-run-worker.mjs` 已进入 runtime,可检查 worker LaunchAgent、queue lag、最近失败 run;生产验证 `ok=true`。 - P6.11 Single-user Rollout Policy: 已完成默认关闭的 code-run 放量策略门禁,支持 user allowlist、task type allowlist、required validation,并在 runtime/status 暴露当前策略。 - P6.12 All-user Long-running Gray: 已完成,生产已默认对所有用户开启 H5 code-run 灰度,external worker running,普通测试用户真实 code-run 已成功生成 MindSpace 产物。 - P6.13 Agent Run Auto-pause Guard: 已完成,新增 `agent-run-guard.mjs` 和 LaunchAgent,按失败数/队列积压/running 超时自动关闭 code-run gate 并停 worker。 - P6.15 H5 Code-run Validation Metadata: 已完成,H5 code-run 自动声明 `.memind/agent-runs/.json` receipt 校验,生产已恢复 `MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION=1` 并通过普通测试用户真实路径。 - P6.17 Controlled Worker Concurrency Canary: 已完成,external worker 短窗口提升到并发 2,两条真实 code-run 同时 running 并全部通过 receipt validation。 - P6.18 Concurrency Rollout Policy: 已完成,生产 worker 和 Portal runtime/status 已对齐到并发 2,worker KeepAlive 保持开启,保留回滚备份。 - P6.19 Task-level Artifact Validation: 已完成第一步,H5 code-run 会在 receipt 之外追加可推断的 `public/*.html` 目标文件校验,页面编辑 code-run 会追加 page-edit task receipt,生产真实 canary 三项 expectedFiles 全部通过。 - P8.1 Queue Lease / Stuck Run Recovery: 已完成第一步,worker dispatch 前自动回收超时 running run,新增 `--recover-stale` dry-run/apply 运维入口,生产 synthetic stale run 验证通过。 - P8.2 Portal DB/Auth Transient Error Hardening: 已完成第一步,session attach、`/auth/status` 和 API auth middleware 已捕获 DB/auth 瞬时错误,生产部署后 live health、auth/status、runtime/status、guard 和 SLO 验证通过。 - P8.3 Worker Lease Heartbeat: 已完成第一步,running run 执行期间写入 `worker_heartbeat` event,runtime/status、worker check、guard 和 stale recovery 均优先使用 heartbeat age,生产 synthetic 与真实 worker canary 验证通过。 - P8.4 Long-running Gray Soak and Heartbeat SLO: 已完成第二步,runtime SLO JSON/Markdown 报告加入 agent-run heartbeat 摘要和 failure 条件,并安装独立小时级 soak 快照;继续保持并发 2 soak,不扩大灰度。 - P5.15 Active Stream TTL Reconcile: 已按用户要求跳过,暂不做报表/定时 reconcile。 - P5 Worker Pool 运维化: 已完成第一步,Redis Router 支持 worker drain。 - P5.9 First-token Latency EWMA: 已完成,StreamController 会把首个 SSE chunk 延迟写入 Redis,SLO 报告已展示。 - P5.10 First-token Latency 趋势报表: 已完成 Redis rolling samples,runtime/SLO 已展示最近 5 分钟和 1 小时 p50/p95。 - P5.11 SLO 日报: 已完成 `runtime-slo-report.mjs --write-report`,可输出 JSON/Markdown 运维报告。 - P5.12 SLO 日报定时器: 已完成 LaunchAgent,每日 23:55 自动生成 runtime SLO JSON/Markdown 报告。 - 生产同步分支: 已从远程 `origin/main` 新建干净副本和分支 `memind-streaming-runtime-20260702`,用于远程开发机后续直接拉取。 - P3.5/P5.5 生产化补强: 已完成 runtime metrics、健康检查脚本、drain 运维脚本、自动采样 LaunchAgent 和 SLO 只读报告。 ## 目标 把当前 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.cn`;`m.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 代理尚未完整处理 `flushHeaders`、`X-Accel-Buffering`、客户端断开 abort、写入 backpressure 和统一 pipeline 收尾。 ## 目标架构 ```mermaid 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: ```text 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 ``` 调度分数: ```text 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 操作: ```bash 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-8`、`no-cache, no-transform`、`X-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.cn`,`VITE_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//events` 和 `/api/agent/runs//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 300s` 和 `proxy_send_timeout 300s`。 - 微信菜单脚本默认入口改为 `https://mm.tkmind.cn` 和 `https://mm.tkmind.cn/space`。 - `server.mjs` 内 `H5_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::worker` - `memind:runtime:session::target` - SSE 打开/关闭维护 Redis active stream: - `memind:runtime:worker::active_streams` - `memind:runtime:worker::heartbeat` - `memind:runtime:stream::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=` - `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 的 `target`、`activeStreams`、`activeSessions`、`ewmaFirstTokenMs`、`errorRate`、`memoryPressure`、`heartbeat` - 每个 `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=true`,namespace 为 `memind:runtime`。 - 返回中四个目标 `18006..18009` 均 `healthy: 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` 曾将 `aider`、`openhands` 覆盖为 `allowed=1`。 - 已将 `h5_capability_grants` 中 `subject_type='role' AND subject_id='user'` 的 `aider`、`openhands` 改为 `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::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.git` 的 `origin/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=user` 的 `aider` / `openhands` 均为 `false`。 - 显式白名单用户数为 `2`。 - 白名单用户 chat mode 不含 Aider/OpenHands,code 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`。 ### 2026-07-02 P4.6 Code Task 入口接线 目标: - 在不引入新表、不改生产数据的前提下,让显式代码任务可以进入 `toolMode='code'`。 - 普通 H5 聊天继续默认 `toolMode='chat'`,不注入 Aider/OpenHands。 - 对已有 session 的 code run 也先 reconcile 到 code policy,再提交用户消息。 已完成: - `POST /api/agent/runs` 支持 `tool_mode` / `toolMode` 与 `task_type` / `taskType`。 - 支持的 code mode alias: - `code` - `code-task` - `code_task` - `code-tool` - `code_tool` - `code_tool_task` - 未知 `tool_mode` 返回 400,避免误开高权限工具。 - run 创建时将运行模式写入既有 `user_message_json.metadata.memindRun`: - `toolMode` - `taskType` - 新建 code run session 时使用 `userAuth.getCodeAgentSessionPolicy(userId)`。 - 提交 code run 到已有 session 前,`submitSessionReplyForUser(..., { toolMode: 'code' })` 会按 code policy reconcile。 安全边界: - 本阶段不写数据库 schema。 - 本阶段不修改或删除 `/Users/john/Project/Memind/MindSpace`。 - Aider/OpenHands 仍只对白名单用户在 code mode 下按 policy 注入。 - 普通 chat mode 仍保持无 Aider/OpenHands。 本地验证: - `node --check agent-run-gateway.mjs agent-run-routes.mjs tkmind-proxy.mjs` 通过。 - `node --test agent-run-gateway.test.mjs agent-run-routes.test.mjs capabilities.test.mjs policies.test.mjs user-memory-profile.test.mjs tkmind-proxy.test.mjs user-publish.test.mjs message-stream.test.mjs` 通过,80 tests pass。 生产部署: - 生产代码备份: `/Users/john/Project/memind_backups/20260702-071604-pre-p46-code-task-entry`。 - 已将 `.runtime/portal/server.mjs` 部署为 `/Users/john/Project/Memind/server.mjs`。 - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - 未修改 PG 数据,未修改或删除 `/Users/john/Project/Memind/MindSpace`。 生产验证: - `https://mm.tkmind.cn/api/status` 返回 `ok`。 - `https://mm.tkmind.cn/api/runtime/status` 返回 router enabled,四个 goosed target healthy。 - `scripts/check-stream-runtime.mjs` 返回 `ok=true`。 - `scripts/check-tool-runtime.mjs` 返回 `ok=true`,普通 chat 不注入 Aider/OpenHands,code mode 对白名单用户可注入。 - `scripts/runtime-worker-drain.mjs status` 返回 `ok=true`,四个 worker drain=false。 - 观察项: `goosed-1 activeStreams=2`,无 error;未强制重置,避免误伤生产中的真实 stream。 ### 2026-07-02 P4.7 H5 Code Run 灰度入口 目标: - 将 H5 侧 agent run 创建入口接到 P4.6 的 `tool_mode`。 - 默认关闭灰度,避免普通生产聊天自动扩大 Aider/OpenHands 暴露面。 - 远程开发机拉取分支后,可通过构建时 env 开关灰度验证。 已完成: - `createAgentRun()` 支持可选 `toolMode` / `taskType`,请求体映射为: - `tool_mode` - `task_type` - 新增前端 helper `src/utils/agentRunMode.ts`。 - 新增构建时开关: - `VITE_AGENT_CODE_RUNS_ENABLED` - `VITE_AGENT_CODE_RUNS_AUTODETECT` - 开关默认关闭。 - 页面编辑子聊天在 `VITE_AGENT_CODE_RUNS_ENABLED=1` 时传 `tool_mode=code`。 - 普通 H5 聊天仅在 `VITE_AGENT_CODE_RUNS_ENABLED=1` 且 `VITE_AGENT_CODE_RUNS_AUTODETECT=1` 时,按明确代码/仓库/测试/重构类文本命中后传 `tool_mode=code`。 本地验证: - `npm run build` 通过。 - 当前构建未设置灰度 env,因此生产默认行为仍为 chat mode。 安全边界: - 本阶段不改 PG,不改 schema。 - 本阶段不修改或删除 `/Users/john/Project/Memind/MindSpace`。 - `tool_mode=code` 到达后端后仍受 P4.6/P4.5 policy 约束;非白名单用户不会获得 Aider/OpenHands。 ### 2026-07-02 P4.8 Code Run 灰度前只读检查 目标: - 固化灰度前检查步骤,避免靠人工记忆判断 H5 code-run 链路是否安全。 - 检查过程不创建 agent run,不写数据库,不修改 MindSpace。 已完成: - 新增只读脚本 `scripts/check-agent-code-run-entry.mjs`。 - 检查内容: - 当前 H5 `dist` bundle 是否包含 `/agent/runs` 和 `tool_mode` 入口。 - `/api/runtime/status.toolRuntime` 是否保持: - `defaultMode='chat'` - `codeToolMode='code'` - `chatInjectsCodeTools=false` - `role=user` 的 `aider` / `openhands` 默认是否仍为 false。 - 白名单用户 code mode 是否具备代码工具能力。 - 明确输出 `writes.database=false`、`writes.mindSpace=false`、`createsAgentRun=false`。 - `scripts/build-portal-runtime.mjs` 已将该脚本复制到 runtime artifact,并在 RUNBOOK 中列出。 验证: - `node --check scripts/check-agent-code-run-entry.mjs scripts/build-portal-runtime.mjs` 通过。 - 使用生产 env 验证: - `MEMIND_ENV_FILE=/Users/john/Project/Memind/.env node scripts/check-agent-code-run-entry.mjs` - 返回 `ok=true`。 ### 2026-07-02 P4.9 Code Run 后端灰度门禁 目标: - 在 H5 构建开关之外,再增加 Portal 后端开关,防止客户端直接提交 `tool_mode=code` 绕过前端灰度。 - 默认关闭,不影响普通 chat run。 已完成: - 新增后端 env: - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - `POST /api/agent/runs` 收到 `tool_mode=code` 且后端开关未开启时返回 403: - `代码任务灰度未开启` - 普通 `tool_mode=chat` 不受该开关影响。 - `/api/runtime/status.toolRuntime` 增加 `codeRunsEnabled`。 - `scripts/check-agent-code-run-entry.mjs` 增加后端开关一致性检查。 - `.env.example` 增加后端灰度开关说明。 验证: - `node --check agent-run-routes.mjs tkmind-proxy.mjs scripts/check-agent-code-run-entry.mjs` 通过。 - `node --test agent-run-routes.test.mjs agent-run-gateway.test.mjs` 通过,14 tests pass。 - 相关回归 `capabilities/policies/user-memory/tkmind-proxy/message-stream` 通过,61 tests pass。 生产部署: - 生产代码备份: `/Users/john/Project/memind_backups/20260702-072452-pre-p49-code-run-server-gate`。 - 已覆盖 `/Users/john/Project/Memind/server.mjs` 和 `scripts/check-agent-code-run-entry.mjs`。 - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - 未修改 PG 数据,未修改或删除 `/Users/john/Project/Memind/MindSpace`。 生产验证: - `https://mm.tkmind.cn/api/status` 返回 `ok`。 - `/api/runtime/status.toolRuntime.codeRunsEnabled=false`。 - 四个 goosed target healthy。 - `scripts/check-agent-code-run-entry.mjs` 返回 `ok=true`。 - `scripts/check-stream-runtime.mjs` 返回 `ok=true`。 - `scripts/runtime-worker-drain.mjs status` 返回 `ok=true`,四个 worker drain=false。 - 观察项: worker `activeStreams` 有非零计数且无 error;未强制重置,避免误伤生产中的真实 stream。 ### 2026-07-02 P5.6 Worker activeStreams 收敛工具 目标: - 处理 Redis 中 `active_streams` 因异常断流或进程重启可能残留的问题。 - 默认只读,不自动清生产状态。 - 只有显式 `--apply` 且 stream 明显超过 TTL 时才做收敛。 已完成: - `scripts/runtime-worker-drain.mjs` 新增动作: - `reconcile` - `reconcile --apply` - `--stale-ms=` - 默认 TTL: - `MEMIND_RUNTIME_STALE_STREAM_MS` - 未设置时为 `900000ms`。 - `reconcile` 默认 dry-run,只报告: - `stale` - `staleAgeMs` - `lastStreamReconciledAt` - `streamReconcileCount` - `reconciled` - 只有 `activeStreams > 0` 且 `last_stream_started_at` 超过 TTL,并且没有更新的 `last_stream_ended_at` 时,才被视为 stale。 - `MEMIND_ENV_FILE` 已被 `runtime-worker-drain.mjs` 支持,便于在干净分支读取生产 env 做只读检查。 - runtime RUNBOOK 模板增加: - `node scripts/runtime-worker-drain.mjs reconcile` - `node scripts/runtime-worker-drain.mjs reconcile --apply` 验证: - `node --check scripts/runtime-worker-drain.mjs scripts/build-portal-runtime.mjs` 通过。 - 使用生产 env dry-run: - `MEMIND_ENV_FILE=/Users/john/Project/Memind/.env node scripts/runtime-worker-drain.mjs reconcile` - 返回 `ok=true`。 - 四个 worker 均列出。 - 当前无 stale worker,未执行 apply。 生产部署: - 生产脚本备份: `/Users/john/Project/memind_backups/20260702-072835-pre-p56-worker-reconcile`。 - 已覆盖 `/Users/john/Project/Memind/scripts/runtime-worker-drain.mjs` 和 `RUNBOOK.txt`。 - 未重启 Portal。 - 未修改 PG 数据,未修改或删除 `/Users/john/Project/Memind/MindSpace`。 生产验证: - `node scripts/runtime-worker-drain.mjs reconcile` 返回 `ok=true`。 - `dryRun=true`。 - 四个 worker 均列出。 - 当前无 stale worker,未执行 apply。 - `https://mm.tkmind.cn/api/status` 返回 `ok`。 ### 2026-07-02 P5.7 Worker Metrics 采样接入 目标: - 将 goosed worker 的进程/容器指标写入 Redis runtime state。 - 让 Router score 不只依赖 `active_streams`,同时纳入 CPU / memory / fd pressure。 - 保持 PG 和 MindSpace 零写入。 已完成: - 新增 `scripts/runtime-worker-metrics.mjs`。 - 支持: - `status`:采样但不写 Redis。 - `sample`:采样并写入 Redis。 - `--dry-run`:强制只读。 - 对 Docker worker `goosed-prod-1..4` 采集: - `cpuLoad` - `memoryPressure` - `fdPressure` - `fdCount` - `containerPids` - `containerHealth` - `containerHostPid` - `metricsSampledAt` - `tkmind-proxy` Router score 增加: - `cpu_load * 2` - `fd_pressure * 2` - `/api/runtime/status.router.workers[]` 增加 metrics 字段。 - runtime artifact 和 RUNBOOK 增加: - `node scripts/runtime-worker-metrics.mjs status` - `node scripts/runtime-worker-metrics.mjs sample` 验证: - `node --check scripts/runtime-worker-metrics.mjs tkmind-proxy.mjs scripts/build-portal-runtime.mjs` 通过。 - `MEMIND_ENV_FILE=/Users/john/Project/Memind/.env node scripts/runtime-worker-metrics.mjs status` 返回 `ok=true`。 - `MEMIND_ENV_FILE=/Users/john/Project/Memind/.env node scripts/runtime-worker-metrics.mjs sample` 返回 `ok=true`,并写入 Redis runtime state。 - 相关回归测试 58 tests pass。 生产部署: - 生产代码备份: `/Users/john/Project/memind_backups/20260702-073424-pre-p57-worker-metrics-final`。 - 已覆盖: - `/Users/john/Project/Memind/server.mjs` - `/Users/john/Project/Memind/RUNBOOK.txt` - `/Users/john/Project/Memind/scripts/runtime-worker-drain.mjs` - `/Users/john/Project/Memind/scripts/runtime-worker-metrics.mjs` - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - 未修改 PG 数据,未修改或删除 `/Users/john/Project/Memind/MindSpace`。 生产验证: - `node scripts/runtime-worker-metrics.mjs sample` 返回 `ok=true`,四个 `goosed-prod-*` 均 `healthy`。 - `/api/runtime/status.router.workers[]` 已显示: - `cpuLoad` - `memoryPressure` - `fdPressure` - `fdCount` - `containerPids` - `containerHealth` - `metricsSampledAt` - 新 `score` - `https://mm.tkmind.cn/api/status` 返回 `ok`。 - `scripts/check-stream-runtime.mjs` 返回 `ok=true`。 - `scripts/check-agent-code-run-entry.mjs` 返回 `ok=true`。 - `scripts/runtime-worker-drain.mjs reconcile` 返回 `ok=true`,`dryRun=true`。 ### 2026-07-02 P5.8 自动采样与 SLO 只读报告 目标: - 将 P5.7 的手动 worker metrics 采样改为 LaunchAgent 定时采样。 - 增加一份生产 SLO 快照脚本,作为每次灰度/发布前后的统一只读检查。 - PG 和 MindSpace 允许只读统计,不删除、不修改用户数据。 已完成: - 新增 `scripts/install-runtime-metrics-agent.sh`。 - 安装 LaunchAgent: `cn.tkmind.memind-runtime-metrics`。 - 默认每 60 秒执行 `node scripts/runtime-worker-metrics.mjs sample`。 - 日志: `~/Library/Logs/memind-runtime-metrics.log`。 - 新增 `scripts/runtime-slo-report.mjs`。 - 读取 `/api/runtime/status`。 - 只读统计 PG 表行数。 - 只读统计 `/Users/john/Project/Memind/MindSpace` 和 `data/mindspace` 文件/目录/字节数。 - 读取 Redis runtime key 摘要。 - 检查 worker target health、container health、metrics freshness、stream errors、chat/code tool gate。 - 输出 `writes.database=false`、`writes.mindSpace=false`、`writes.redis=false`。 - runtime artifact 和 RUNBOOK 增加: - `bash scripts/install-runtime-metrics-agent.sh` - `node scripts/runtime-slo-report.mjs` 验证: - `node --check scripts/runtime-slo-report.mjs scripts/runtime-worker-metrics.mjs scripts/build-portal-runtime.mjs` 通过。 - `bash -n scripts/install-runtime-metrics-agent.sh` 通过。 - 使用生产 env 只读验证: - `MEMIND_ENV_FILE=/Users/john/Project/Memind/.env node scripts/runtime-slo-report.mjs` - 返回 `ok=true`。 - 正确统计 `/Users/john/Project/Memind/MindSpace`。 生产部署: - 生产脚本备份: `/Users/john/Project/memind_backups/20260702-073927-pre-p58-runtime-slo-agent`。 - 已覆盖: - `/Users/john/Project/Memind/RUNBOOK.txt` - `/Users/john/Project/Memind/scripts/runtime-worker-metrics.mjs` - `/Users/john/Project/Memind/scripts/install-runtime-metrics-agent.sh` - `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs` - 已安装 LaunchAgent: - label: `cn.tkmind.memind-runtime-metrics` - plist: `/Users/john/Library/LaunchAgents/cn.tkmind.memind-runtime-metrics.plist` - interval: `60s` - log: `/Users/john/Library/Logs/memind-runtime-metrics.log` - LaunchAgent 执行状态: - `launchctl print gui/$(id -u)/cn.tkmind.memind-runtime-metrics` 显示最近运行退出码为 `0`。 - 日志显示 `runtime-worker-metrics.mjs sample` 成功。 - 生产 SLO 只读报告: - `ok=true` - `failures=[]` - 四个 worker `metricsFresh=true` - `/Users/john/Project/Memind/MindSpace`: `files=2143`, `dirs=1595`, `bytes=122160654` - `/Users/john/Project/Memind/data/mindspace`: `files=5281`, `dirs=7651`, `bytes=270214686` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - 未修改 PG 用户数据,未删除或修改 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P5.9 First-token Latency EWMA 目标: - 将 H5 SSE 请求从进入 StreamController 到收到首个 upstream SSE body chunk 的耗时写入 Redis。 - 让 Router score、`/api/runtime/status` 和 SLO 报告都能看到 worker 级 first-token 指标。 - 不写 PG,不修改或删除 MindSpace 用户数据。 已完成: - `tkmind-proxy.mjs` 增加 first-token probe。 - 首个 non-empty upstream chunk 到达时异步记录,不阻塞 token 下发。 - 写入 Redis: - `worker:{id}:ewma_first_token_ms` - `worker:{id}:last_first_token_ms` - `worker:{id}:last_first_token_at` - `worker:{id}:first_token_count` - EWMA 计算: `next = previous * 0.8 + sample * 0.2`,首次样本直接使用 sample。 - `/api/runtime/status.router.workers[]` 增加: - `ewmaFirstTokenMs` - `lastFirstTokenMs` - `lastFirstTokenAt` - `firstTokenCount` - `scripts/runtime-slo-report.mjs` 增加 first-token 指标展示和基础一致性检查。 验证: - `node --check tkmind-proxy.mjs scripts/runtime-slo-report.mjs scripts/build-portal-runtime.mjs` 通过。 - `node --test tkmind-proxy.test.mjs message-stream.test.mjs` 通过,12 tests pass。 - `node scripts/build-portal-runtime.mjs` 通过。 生产部署: - 生产代码备份: `/Users/john/Project/memind_backups/20260702-074436-pre-p59-first-token-ewma`。 - 已覆盖: - `/Users/john/Project/Memind/server.mjs` - `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs` - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`;Portal 随后正常监听 `:8081`,公网 `https://mm.tkmind.cn/api/status` 返回 `ok`。 生产验证: - `/api/runtime/status` 返回 `ok=true`。 - 已观测到真实首 token 样本: - `goosed-4.ewmaFirstTokenMs=125` - `goosed-4.lastFirstTokenMs=125` - `goosed-4.firstTokenCount=1` - `scripts/runtime-slo-report.mjs` 返回: - `ok=true` - `failures=[]` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - PG 仅由 SLO 脚本执行只读计数;未修改 PG 用户数据。 - MindSpace 仅由 SLO 脚本执行只读统计;未删除或修改 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P6.0 Tool Gateway Queue v0 目标: - 将 `/agent/runs` 的后台执行变成受控队列,作为 Aider/OpenHands 后续拆分的最小 Tool Gateway。 - 默认限制并发,避免 code task 放量后压垮 goosed 或本机工具进程。 - 增加运行超时和 runtime/SLO 可观测状态。 - 不做 schema migration,不修改 PG 用户数据,不删除或修改 MindSpace 用户数据。 已完成: - `agent-run-gateway.mjs` 增加本机队列调度器: - `maxConcurrentRuns` - `runTimeoutMs` - `inFlight` - `pendingDispatches` - `statusCounts` - 默认配置: - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY` 未设置时为 `1` - `MEMIND_AGENT_RUN_TIMEOUT_MS` 未设置时为 `900000ms` - 执行超时时: - run 标记为 `failed` - 写入 `timeout` event - 不重试超时任务 - `/api/runtime/status.toolRuntime.queue` 增加队列状态。 - `scripts/runtime-slo-report.mjs` 增加队列状态展示和基础一致性检查。 - 普通聊天仍不注入 Aider/OpenHands。 - `MEMIND_AGENT_CODE_RUNS_ENABLED` 仍为 false,生产 code run 未放量。 验证: - `node --check agent-run-gateway.mjs agent-run-routes.mjs server.mjs scripts/runtime-slo-report.mjs` 通过。 - `node --test agent-run-gateway.test.mjs agent-run-routes.test.mjs` 通过,17 tests pass。 - `node scripts/build-portal-runtime.mjs` 通过。 生产部署: - 生产代码备份: `/Users/john/Project/memind_backups/20260702-074955-pre-p60-tool-queue-v0`。 - 已覆盖: - `/Users/john/Project/Memind/server.mjs` - `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs` - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`;Portal 随后正常监听 `:8081`,公网 `https://mm.tkmind.cn/api/status` 返回 `ok`。 生产验证: - `/api/runtime/status.toolRuntime.queue` 返回: - `maxConcurrentRuns=1` - `runTimeoutMs=900000` - `inFlight=0` - `pendingDispatches=0` - `statusCounts={}` - `/api/runtime/status.toolRuntime.codeRunsEnabled=false`。 - `scripts/runtime-slo-report.mjs` 返回: - `ok=true` - `failures=[]` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - PG 仅由 SLO 脚本执行只读计数;未修改 PG 用户数据。 - MindSpace 仅由 SLO 脚本执行只读统计;未删除或修改 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P6.1 Tool Worker v1 骨架 目标: - 为 `/agent/runs` 增加外部 worker 接管能力。 - Portal 默认仍保持内部 `autoDispatch=true`,避免生产行为突然切换。 - 外部 worker 默认只在显式启动时运行,不安装 LaunchAgent,不自动放量。 - 不做 schema migration,不修改 PG 用户数据,不删除或修改 MindSpace 用户数据。 已完成: - `agent-run-gateway.mjs` 增加: - `autoDispatch` 显式配置。 - `dispatchQueuedRuns({ limit })`,扫描现有 `queued/retryable` run 并走同一队列控制。 - 队列满载时不预取 DB 任务,等待下一轮 poll。 - `server.mjs` 增加 `MEMIND_AGENT_RUN_AUTODISPATCH` 配置: - 默认 `1`,保持 Portal 内部 dispatch。 - 后续外部 worker canary 时可设为 `0`。 - 新增 `scripts/agent-run-worker.mjs`: - `--status`: 只读查看队列状态。 - `--once`: 扫描并调度一批 queued/retryable run。 - 常驻模式: 按 `MEMIND_AGENT_RUN_WORKER_POLL_MS` 轮询。 - `scripts/build-portal-runtime.mjs` 将 worker 打包成单文件 runtime: - `.runtime/portal/scripts/agent-run-worker.mjs` - `.env.example` 增加: - `MEMIND_AGENT_RUN_AUTODISPATCH` - `MEMIND_AGENT_RUN_WORKER_POLL_MS` - `MEMIND_AGENT_RUN_WORKER_BATCH_SIZE` - RUNBOOK 增加: - `node scripts/agent-run-worker.mjs --status` - `node scripts/agent-run-worker.mjs --once` 验证: - `node --check agent-run-gateway.mjs server.mjs scripts/agent-run-worker.mjs scripts/build-portal-runtime.mjs` 通过。 - `node --test agent-run-gateway.test.mjs agent-run-routes.test.mjs` 通过,19 tests pass。 - `node scripts/build-portal-runtime.mjs` 通过,worker 被打成单文件 runtime。 生产部署: - 生产代码备份: `/Users/john/Project/memind_backups/20260702-075904-pre-p61-agent-run-worker-v1`。 - 已覆盖: - `/Users/john/Project/Memind/server.mjs` - `/Users/john/Project/Memind/scripts/agent-run-worker.mjs` - `/Users/john/Project/Memind/RUNBOOK.txt` - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`;Portal 随后正常监听 `:8081`,公网 runtime status 返回 `ok=true`。 生产验证: - `/api/runtime/status.toolRuntime.queue.autoDispatch=true`。 - `/api/runtime/status.toolRuntime.codeRunsEnabled=false`。 - `node scripts/agent-run-worker.mjs --status` 返回 `ok=true`,且 worker 自身 `autoDispatch=false`。 - `scripts/runtime-slo-report.mjs` 返回: - `ok=true` - `failures=[]` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - PG 仅由 status/SLO 执行只读检查;未修改 PG 用户数据。 - MindSpace 仅由 SLO 脚本执行只读统计;未删除或修改 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P5.10 First-token Latency 趋势报表 目标: - 将 P5.9 的 worker-level EWMA 扩展成短窗口趋势指标。 - 在 Redis runtime state 中保留最近 1 小时 first-token 样本。 - `/api/runtime/status` 和 SLO 报告展示最近 5 分钟/1 小时 p50、p95。 - 不写 PG,不修改或删除 MindSpace 用户数据。 已完成: - `tkmind-proxy.mjs` 在首个 upstream SSE chunk 到达时写入 Redis sorted set: - `worker:{id}:first_token_samples` - sorted set score 为时间戳,member 带 latency sample。 - 自动清理 1 小时前样本。 - key TTL 为 2 小时。 - `/api/runtime/status.router.workers[]` 增加: - `firstToken5m.count` - `firstToken5m.p50Ms` - `firstToken5m.p95Ms` - `firstToken1h.count` - `firstToken1h.p50Ms` - `firstToken1h.p95Ms` - `scripts/runtime-slo-report.mjs` 增加 first-token 窗口指标输出。 验证: - `node --check tkmind-proxy.mjs scripts/runtime-slo-report.mjs scripts/build-portal-runtime.mjs` 通过。 - `node --test tkmind-proxy.test.mjs message-stream.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs` 通过,31 tests pass。 - `node scripts/build-portal-runtime.mjs` 通过。 生产部署: - 生产代码备份: `/Users/john/Project/memind_backups/20260702-080546-pre-p510-first-token-trends`。 - 已覆盖: - `/Users/john/Project/Memind/server.mjs` - `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs` - `/Users/john/Project/Memind/scripts/agent-run-worker.mjs` - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - 重启窗口内 `mm.tkmind.cn` 曾短暂返回 nginx `502`;Portal 随后正常监听 `:8081`,公网 `https://mm.tkmind.cn/api/status` 返回 `ok`。 生产验证: - `/api/runtime/status` 返回 `ok=true`。 - 已观测到真实窗口样本: - `goosed-4.ewmaFirstTokenMs=101` - `goosed-4.firstToken5m.count=1` - `goosed-4.firstToken5m.p50Ms=188` - `goosed-4.firstToken5m.p95Ms=188` - `goosed-4.firstToken1h.count=1` - `goosed-4.firstToken1h.p50Ms=188` - `goosed-4.firstToken1h.p95Ms=188` - `scripts/runtime-slo-report.mjs` 返回: - `ok=true` - `failures=[]` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - PG 仅由 SLO 脚本执行只读计数;未修改 PG 用户数据。 - MindSpace 仅由 SLO 脚本执行只读统计;未删除或修改 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P5.11 SLO 日报 目标: - 将即时 SLO 快照落盘为运维报告,方便跨天追踪。 - 默认仍只读;只有显式 `--write-report` 时写报告目录。 - 不写 PG,不修改或删除 MindSpace 用户数据。 已完成: - `scripts/runtime-slo-report.mjs` 增加: - `--write-report` - `--report-dir ` - Markdown 报告 - JSON 报告 - 默认报告目录: - `/Users/john/Project/Memind/reports/runtime-slo` - 报告内容包含: - runtime ok/failures - worker health - active streams - stream errors - first-token 5m/1h p50/p95 - tool queue state - writes 声明 验证: - `node --check scripts/runtime-slo-report.mjs` 通过。 - `node scripts/runtime-slo-report.mjs --help` 正常输出。 - 使用生产 env 执行 `--write-report` 返回 `ok=true`。 生产部署: - 生产脚本备份: `/Users/john/Project/memind_backups/20260702-080820-pre-p511-slo-daily-report`。 - 已覆盖: - `/Users/john/Project/Memind/scripts/runtime-slo-report.mjs` - 未重启 Portal。 生产验证: - `node scripts/runtime-slo-report.mjs --write-report` 返回: - `ok=true` - `failures=[]` - `reportFiles.json=/Users/john/Project/Memind/reports/runtime-slo/2026-07-02T00-08-26-316Z.json` - `reportFiles.markdown=/Users/john/Project/Memind/reports/runtime-slo/2026-07-02T00-08-26-316Z.md` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - `writes.report=true` - PG 仅由 SLO 脚本执行只读计数;未修改 PG 用户数据。 - MindSpace 仅由 SLO 脚本执行只读统计;未删除或修改 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P5.12 SLO 日报定时器 目标: - 将 P5.11 的手动 SLO 日报落盘变成每日自动执行。 - 只写运维报告目录,不写 PG,不修改或删除 MindSpace 用户数据。 已完成: - 新增 `scripts/install-runtime-slo-report-agent.sh`。 - 安装 LaunchAgent: - label: `cn.tkmind.memind-runtime-slo-report` - plist: `/Users/john/Library/LaunchAgents/cn.tkmind.memind-runtime-slo-report.plist` - schedule: 每日 `23:55` - command: `node scripts/runtime-slo-report.mjs --write-report --report-dir /Users/john/Project/Memind/reports/runtime-slo` - log: `/Users/john/Library/Logs/memind-runtime-slo-report.log` - `.env.example` 增加: - `MEMIND_RUNTIME_REPORT_DIR` - `MEMIND_RUNTIME_SLO_HOUR` - `MEMIND_RUNTIME_SLO_MINUTE` - runtime artifact 和 RUNBOOK 增加: - `bash scripts/install-runtime-slo-report-agent.sh` 验证: - `bash -n scripts/install-runtime-slo-report-agent.sh` 通过。 - `node --check scripts/build-portal-runtime.mjs scripts/runtime-slo-report.mjs` 通过。 - `node scripts/build-portal-runtime.mjs` 通过。 生产部署: - 生产脚本备份: `/Users/john/Project/memind_backups/20260702-081100-pre-p512-slo-report-agent`。 - 已覆盖: - `/Users/john/Project/Memind/RUNBOOK.txt` - `/Users/john/Project/Memind/scripts/install-runtime-slo-report-agent.sh` - 已执行: - `bash scripts/install-runtime-slo-report-agent.sh` - 未重启 Portal。 生产验证: - `launchctl print gui/$(id -u)/cn.tkmind.memind-runtime-slo-report`: - `last exit code = 0` - `runs = 1` - `Hour = 23` - `Minute = 55` - 已生成日报: - `/Users/john/Project/Memind/reports/runtime-slo/2026-07-02T00-11-06-214Z.json` - `/Users/john/Project/Memind/reports/runtime-slo/2026-07-02T00-11-06-214Z.md` - 日志中 SLO: - `failures=[]` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - `writes.report=true` - PG 仅由 SLO 脚本执行只读计数;未修改 PG 用户数据。 - MindSpace 仅由 SLO 脚本执行只读统计;未删除或修改 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P6.2 Tool Worker Canary 目标: - 短窗口验证外部 `agent-run-worker.mjs --once` 能接管真实 `/agent/runs` queued run。 - 不打开 code run,不启用 Aider/OpenHands。 - 不删除 PG 数据,不修改或删除 MindSpace 用户数据。 执行: - 生产 `.env` 临时增加: - `MEMIND_AGENT_RUN_AUTODISPATCH=0` - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - runtime status 确认: - `toolRuntime.queue.autoDispatch=false` - `toolRuntime.codeRunsEnabled=false` - 使用 `testuser1` 创建 chat-mode canary run。 - 执行: - `node scripts/agent-run-worker.mjs --once --limit 1` 过程中发现并修复: - `--once` 原先调度后会立即关闭 DB pool,导致异步 run 标记状态时报 `Pool is closed`。 - 已修复为等待 `inFlight=0` 后再退出。 - 外部 worker 模式下 retry 不应使用进程内 `setTimeout`。 - 已修复为 `autoDispatch=false` 时保留 `retryable`,由下一轮 worker poll 接管。 - `--once` 完成后仍有长连接句柄导致 CLI 不退出。 - 已修复为 `--status/--once` 完成后显式退出。 - 手工 canary payload 需要完整 goosed message 字段: - `metadata.userVisible=true` - `metadata.agentVisible=true` - `created=` - `request_id=` 验证: - `node --test agent-run-gateway.test.mjs agent-run-routes.test.mjs` 通过,19 tests pass。 - 成功 canary: - run id: `8e64ff4b-c043-475e-b60b-ae7466111da9` - session id: `20260702_2` - final status: `succeeded` - attempts: `3` - 失败的早期 canary 保留为审计记录: - run id: `9f0c713a-0aff-4e67-bf86-0ea2952e9326` - final status: `failed` - 原因: 手工 payload 缺字段 恢复: - 已移除生产 `.env` 中的 `MEMIND_AGENT_RUN_AUTODISPATCH=0`。 - 已重启 Portal。 - runtime status 确认: - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.inFlight=0` - `toolRuntime.queue.pendingDispatches=0` - `toolRuntime.codeRunsEnabled=false` - PG 仅新增 canary run/session/event 审计记录;未删除 PG 用户数据。 - 未修改或删除 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P4.10 Code Run Canary 目标: - 短窗口验证 code-mode agent run 可以被外部 worker 接管并完成。 - 只对白名单用户打开后端 code-run 开关。 - 不打开普通聊天 autodetect,不让普通聊天注入 Aider/OpenHands 工具。 - 验证后恢复默认关闭。 保护边界: - 生产 `.env` 修改前备份: - `/Users/john/Project/memind_backups/20260702-083225-pre-p410-code-run-canary` - PG 只新增 canary run/session/event 审计记录。 - 未删除或修改 PG 用户数据。 - 未修改或删除 `/Users/john/Project/Memind/MindSpace` 用户数据。 执行: - 生产 `.env` 临时增加: - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - 已执行 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal`。 - runtime status 确认: - `toolRuntime.codeRunsEnabled=true` - `toolRuntime.chatInjectsCodeTools=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.inFlight=0` - 使用 john 白名单用户创建 code-mode canary run。 - 执行: - `MEMIND_AGENT_RUN_WORKER_DRAIN_WAIT_MS=180000 node scripts/agent-run-worker.mjs --once --limit 1` 验证: - 成功 canary: - user id: `a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - run id: `805e7422-175e-4eb7-8fb2-df0c29f79c0c` - request id: `b7ef57f3-725c-4a3a-b214-997b29afb86f` - session id: `20260702_3` - final status: `succeeded` - attempts: `1` - run events 已记录: - `queued` - `running` - `session_started` - `succeeded` 恢复: - 已移除生产 `.env` 中的 `MEMIND_AGENT_CODE_RUNS_ENABLED=1`。 - 已重启 Portal。 - runtime status 确认: - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.chatInjectsCodeTools=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.inFlight=0` - `toolRuntime.queue.pendingDispatches=0` - `toolRuntime.queue.statusCounts={}` - `mm.tkmind.cn` runtime health 仍为 `ok=true`。 ### 2026-07-02 P5.13 Runtime SLO Report Retention 目标: - 给 `reports/runtime-slo` 日报目录增加保留周期。 - 只清理运维报告目录下的过期 `.json/.md`。 - 不触碰 PG,不触碰 MindSpace。 执行: - 源码分支新增提交: - `8a53dff chore: prune runtime slo reports` - `scripts/runtime-slo-report.mjs` 新增: - `--prune` - `--retention-days ` - `MEMIND_RUNTIME_SLO_RETENTION_DAYS`,默认 `30` - `scripts/install-runtime-slo-report-agent.sh` 默认安装参数: - `--write-report` - `--prune` - `--retention-days 30` - 生产运行目录脚本已同步。 - 生产脚本修改前备份: - `/Users/john/Project/memind_backups/20260702-083917-pre-p513-slo-prune` 验证: - LaunchAgent: - `cn.tkmind.memind-runtime-slo-report` - daily `23:55` - arguments 已包含 `--prune --retention-days 30` - last exit code: `0` - 最新报告: - `/Users/john/Project/Memind/reports/runtime-slo/2026-07-02T00-39-23-109Z.json` - `ok=true` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - `writes.report=true` - `writes.reportPrune=true` - `reportPrune.deleted=[]` - `reportPrune.errors=[]` 边界: - 清理范围只限 report dir 顶层 `.json/.md`。 - 不递归清理。 - 不删除非报告文件。 - 未删除 PG 数据。 - 未修改或删除 `/Users/john/Project/Memind/MindSpace` 用户数据。 ### 2026-07-02 P4.11 Code Run User Whitelist Gate 目标: - 为 H5 页面编辑 UI 灰度补齐用户级白名单。 - 防止只开前端 `VITE_AGENT_CODE_RUNS_ENABLED=1` 后,所有页面编辑子聊天都尝试 code mode。 - 后端同步增加用户级 gate,防止客户端绕过前端直接 POST `tool_mode=code`。 实现: - 源码分支新增提交: - `c7d8140 feat: gate code runs by user whitelist` - 前端新增: - `VITE_AGENT_CODE_RUNS_USER_IDS` - `agentCodeRunsEnabledForUser(userId)` - 普通 H5 chat 和页面编辑子聊天调用 `resolveAgentRunOptions()` 时传入当前 user id - 后端新增: - `MEMIND_AGENT_CODE_RUNS_USER_IDS` - `POST /agent/runs` 在 `tool_mode=code` 时校验用户白名单 - 白名单为空时保持原有全局开关语义 验证: - `node --test agent-run-routes.test.mjs agent-run-gateway.test.mjs`: - 21 tests pass - `npm run build`: - 通过 - `scripts/check-agent-code-run-entry.mjs`: - `ok=true` - `MEMIND_AGENT_CODE_RUNS_ENABLED` 未设置 - `VITE_AGENT_CODE_RUNS_ENABLED` 未设置 - `MEMIND_AGENT_CODE_RUNS_USER_IDS` 未设置 - `VITE_AGENT_CODE_RUNS_USER_IDS` 未设置 - `runtimePolicy.codeRunsEnabled=false` - `runtimePolicy.chatInjectsCodeTools=false` 生产发布: - 已发布默认关闭 runtime: - release id: `20260702-085001-a0af049` - git head: `a0af049fa7d5a5a9cf88a09449ab4d3bd43f5cb3` - 发布脚本修复: - `a0af049 fix: pass mindspace link bypass to prod release` - `ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1` 现在会传到远端 post-check - 已按用户要求忽略既有 MindSpace 公开页 docx 附件缺失问题,不做修复 - 发布脚本已完成: - runtime artifact 校验 - live 全目录备份 - 持久目录单独备份 - Portal restart - health check - 105 tunnel health check - 备份: - `/Users/john/Project/backups/memind/memind-full-20260702-085001-a0af049-before.tar.gz` - `/Users/john/Project/backups/memind/memind-persisted-20260702-085001-a0af049-before.tar.gz` - `/Users/john/Project/archives/Memind-source-before-20260702-085001-a0af049` 发布后状态: - `https://mm.tkmind.cn/api/status`: `ok` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.chatInjectsCodeTools=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.inFlight=0` - `toolRuntime.queue.pendingDispatches=0` - SLO report LaunchAgent 仍包含: - `--write-report --prune --retention-days 30` 注意: - 既有生产 MindSpace 公开页缺失 4 个 docx 附件链接,按用户要求忽略,不修复。 - 当前生产已确认运行 release `20260702-085001-a0af049`。 - 未删除 PG 数据。 - 未修改或删除 `/Users/john/Project/Memind/MindSpace` 用户数据。 - 下一步如做真正 H5 UI canary,需要短窗口同时打开: - `VITE_AGENT_CODE_RUNS_ENABLED=1` - `VITE_AGENT_CODE_RUNS_USER_IDS=` - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - `MEMIND_AGENT_CODE_RUNS_USER_IDS=` - 继续保持 `VITE_AGENT_CODE_RUNS_AUTODETECT` 关闭 ### 2026-07-02 P4.12 H5 Page Edit UI Canary 目标: - 短窗口打开 H5 页面编辑 code-mode UI 灰度。 - 只对白名单 john 用户生效。 - 普通聊天 autodetect 继续关闭。 - 验证后恢复默认关闭。 保护边界: - 生产 `.env` 修改前备份: - `/Users/john/Project/memind_backups/20260702-085302-pre-p412-h5-ui-canary` - PG 仅新增 canary run/session/event 审计记录。 - 未删除或修改 PG 用户数据。 - 未修改或删除 `/Users/john/Project/Memind/MindSpace` 用户数据。 canary 打开: - 生产 `.env` 短窗口增加: - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - `MEMIND_AGENT_CODE_RUNS_USER_IDS=a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - 前端 canary bundle 构建环境: - `VITE_AGENT_CODE_RUNS_ENABLED=1` - `VITE_AGENT_CODE_RUNS_USER_IDS=a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - 未设置 `VITE_AGENT_CODE_RUNS_AUTODETECT` - canary release: - `20260702-085319-a0af049` - canary 发布备份: - `/Users/john/Project/backups/memind/memind-full-20260702-085319-a0af049-before.tar.gz` - `/Users/john/Project/backups/memind/memind-persisted-20260702-085319-a0af049-before.tar.gz` - `/Users/john/Project/archives/Memind-source-before-20260702-085319-a0af049` canary 验证: - frontend bundle 已固化: - `VITE_AGENT_CODE_RUNS_ENABLED=true` - john user id whitelist - `page_edit_code_task` - `h5_chat_code_task` - runtime status: - `toolRuntime.codeRunsEnabled=true` - `toolRuntime.chatInjectsCodeTools=false` - `toolRuntime.queue.inFlight=0` - `toolRuntime.queue.pendingDispatches=0` - canary run: - run id: `825720c9-330d-447d-a6ed-afeabbf6e14c` - request id: `03691942-52c1-4612-9588-68840f77b1a6` - user id: `a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - session id: `20260702_4` - status: `succeeded` - attempts: `1` - run events: - `queued` - `running` - `session_started` - `succeeded` 恢复默认关闭: - 已移除生产 `.env` 中: - `MEMIND_AGENT_CODE_RUNS_ENABLED` - `MEMIND_AGENT_CODE_RUNS_USER_IDS` - 已发布默认关闭 bundle: - release id: `20260702-085706-a0af049` - 恢复发布备份: - `/Users/john/Project/backups/memind/memind-full-20260702-085706-a0af049-before.tar.gz` - `/Users/john/Project/backups/memind/memind-persisted-20260702-085706-a0af049-before.tar.gz` - `/Users/john/Project/archives/Memind-source-before-20260702-085706-a0af049` - 恢复后验证: - `https://mm.tkmind.cn/api/status`: `ok` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.chatInjectsCodeTools=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.inFlight=0` - `toolRuntime.queue.pendingDispatches=0` - `.env` 无 `MEMIND_AGENT_CODE_RUNS_*` - frontend bundle 不再包含 john code-run whitelist ### 2026-07-02 P5.14 Worker Self-heartbeat Sidecar 目标: - 将 worker freshness 从单纯外部 60 秒 metrics 采样,升级为常驻 sidecar 主动 heartbeat。 - Router 和 SLO 能直接看到 `heartbeatSource=sidecar`、`heartbeatFresh`、`heartbeatOk`、`heartbeatLatencyMs`。 - 不修改 PG,不修改 MindSpace。 实现: - 源码分支新增提交: - `e24e6ca feat: add runtime worker heartbeat sidecar` - 新增脚本: - `scripts/runtime-worker-heartbeat.mjs` - `scripts/install-runtime-heartbeat-agent.sh` - LaunchAgent: - `cn.tkmind.memind-runtime-heartbeat` - mode: `serve` - interval: `15000ms` - timeout: `5000ms` - ttl: `45000ms` - log: `/Users/john/Library/Logs/memind-runtime-heartbeat.log` - Redis keys: - `worker:{id}:heartbeat` - `worker:{id}:heartbeat_at` - `worker:{id}:heartbeat_source` - `worker:{id}:heartbeat_ok` - `worker:{id}:heartbeat_status_code` - `worker:{id}:heartbeat_latency_ms` - `worker:{id}:heartbeat_error` - Router `/api/runtime/status` 已展示 heartbeat 字段。 - SLO 报告已检查: - `heartbeatFresh` - `heartbeatOk` 生产发布: - release id: `20260702-090343-e24e6ca` - git head: `e24e6ca4a72b4cc16a2074d205727f6d131cff0d` - 发布备份: - `/Users/john/Project/backups/memind/memind-full-20260702-090343-e24e6ca-before.tar.gz` - `/Users/john/Project/backups/memind/memind-persisted-20260702-090343-e24e6ca-before.tar.gz` - `/Users/john/Project/archives/Memind-source-before-20260702-090343-e24e6ca` 验证: - LaunchAgent: - `state = running` - pid: `51413` - program: `/opt/homebrew/opt/node@24/bin/node` - args: `/Users/john/Project/Memind/scripts/runtime-worker-heartbeat.mjs serve` - heartbeat log: - `goosed-1`: `ok=true`, `statusCode=200`, `latencyMs=62` - `goosed-2`: `ok=true`, `statusCode=200`, `latencyMs=46` - `goosed-3`: `ok=true`, `statusCode=200`, `latencyMs=46` - `goosed-4`: `ok=true`, `statusCode=200`, `latencyMs=53` - `/api/runtime/status`: - all workers `heartbeatSource=sidecar` - all workers `heartbeatOk=true` - all workers `heartbeatStatusCode=200` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` - all workers `heartbeatFresh=true` - all workers `metricsFresh=true` 边界: - 未删除 PG 数据。 - 未修改或删除 `/Users/john/Project/Memind/MindSpace` 用户数据。 - 原 60 秒 metrics agent 保留,用于 CPU/memory/fd 指标。 - 新 sidecar 只负责更高频 freshness 和 `/status` 可用性。 ### 2026-07-02 P6.3 Tool Gateway 协议化 目标: - 将 Aider/OpenHands 从 goosed session extension 继续外移为独立 Tool Gateway 执行协议。 - 复用现有 `h5_agent_runs` 和 `h5_agent_run_events`,不新增生产 schema migration。 - 默认关闭,只有 `MEMIND_TOOL_GATEWAY_ENABLED=1` 且 `toolMode=code` 时才走外部执行器。 - 不删除、不修改 PG 用户数据,不删除、不修改 `/Users/john/Project/Memind/MindSpace` 用户文件。 当前落地: - 新增 `tool-gateway.mjs`: - protocol: `agent-run-v1` - executors: `aider`, `openhands` - default executor: `aider` - `repo_refactor,multi_file,complex_repo` 默认走 `openhands` - `MEMIND_TOOL_GATEWAY_DRY_RUN=1` 支持只生成启动计划不执行进程 - `agent-run-gateway.mjs`: - `chat` 和默认关闭状态继续走 Goose 原链路 - `code` + Tool Gateway enabled 时写入事件: - `tool_gateway_dispatch` - `tool_gateway_result` - 外部工具任务完成后标记 run succeeded,不再强制创建 goosed session - `server.mjs` 和 `scripts/agent-run-worker.mjs`: - 已接入同一个 Tool Gateway 对象 - 独立 worker 后续可接管 code run 队列 - `scripts/check-tool-runtime.mjs`: - 已输出 Tool Gateway enable/dry-run/protocol/default executor 状态 - 测试: - `node --test agent-run-gateway.test.mjs tool-gateway.test.mjs` - 结果: `15 passed` - 源码提交: - `21e03d8 feat: add tool gateway protocol` 默认环境: - `MEMIND_TOOL_GATEWAY_ENABLED` 未设置时为关闭。 - `MEMIND_TOOL_GATEWAY_DRY_RUN` 未设置时为关闭。 - 生产发布后若不显式设置 env,不会自动执行 Aider/OpenHands 外部进程。 生产发布: - release id: `20260702-092457-21e03d8` - git head: `21e03d86b5e8ec28a0bd511c484d0618476528a6` - 发布前: - 全目录备份已完成 - 持久目录备份已完成 - 未删除 PG 数据 - 未删除或改写 MindSpace 用户数据 - 发布验证: - `https://mm.tkmind.cn/api/status`: `ok` - `https://mm.tkmind.cn/api/runtime/status`: `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.chatInjectsCodeTools=false` - `toolRuntime.queue.toolGateway.enabled=false` - `toolRuntime.queue.toolGateway.protocol=agent-run-v1` - all workers `heartbeatSource=sidecar` - all workers `heartbeatOk=true` - `scripts/runtime-slo-report.mjs`: `ok=true`, `failures=[]` - `.env` 无 `MEMIND_TOOL_GATEWAY_*` - `.env` 无 `MEMIND_AGENT_CODE_RUNS_*` Dry-run canary: - canary window: - temporarily enabled `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - temporarily enabled `MEMIND_AGENT_CODE_RUNS_USER_IDS=a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - temporarily enabled `MEMIND_TOOL_GATEWAY_ENABLED=1` - temporarily enabled `MEMIND_TOOL_GATEWAY_DRY_RUN=1` - `.env` backup: - `/Users/john/Project/memind_backups/20260702-0927-p63-tool-gateway-dryrun/.env.before` - canary run: - run id: `15dda47e-c282-493b-99df-8a59127404d5` - request id: `p63-dryrun-20260702093311` - user id: `a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - status: `succeeded` - attempts: `1` - `agent_session_id=null` - event proof: - `queued` - `running` - `tool_gateway_dispatch` - `protocol=agent-run-v1` - `taskType=small_patch` - `tool_gateway_result` - `dryRun=true` - `executor=aider` - `exitCode=null` - `succeeded` - cleanup: - restored `.env` from backup - revoked temporary login session - restarted Portal - final `toolRuntime.codeRunsEnabled=false` - final `toolRuntime.queue.toolGateway.enabled=false` - final SLO: `ok=true`, `failures=[]` 边界: - 本阶段未启用生产外部执行器。 - 本阶段未修改 PG schema。 - 本阶段未删除 PG 数据。 - 本阶段未删除或改写 MindSpace 用户数据。 - P5.15 Active Stream TTL Reconcile 已按用户要求跳过。 ### 2026-07-02 P6.4 Tool Gateway Real Canary 目标: - 在专用 canary workspace / 空 git 仓库内验证真实 Aider executor。 - 不打开普通生产 code run。 - 不使用生产用户真实页面。 - 不删除 PG 数据,不删除或改写 `/Users/john/Project/Memind/MindSpace` 用户数据。 第一次真实 canary 发现: - canary dir: - `/Users/john/Project/memind_canary/tool-gateway-p64-20260702-093648` - run id: - `ddfc2666-eb56-48aa-8c7c-a21d99b2ae7c` - request id: - `p64-real-aider-20260702013704` - event proof: - `tool_gateway_dispatch` - `tool_gateway_result` - `dryRun=false` - `executor=aider` - `exitCode=0` - 实际问题: - Aider 进程退出码为 0,但 `.aider.chat.history.md` 显示 LiteLLM 报错: - `LLM Provider NOT provided` - model was `deepseek-reasoner` - 未生成目标 `P64_CANARY.md`,只生成 `.aider*` 和 `.gitignore`。 - 结论: - Tool Gateway 进程协议跑通。 - Aider 模型名映射不正确,真实产物 canary 未通过。 修复: - 源码提交: - `0cfd2d3 fix: map deepseek models for aider` - 修复内容: - Aider executor 对 `custom_deepseek` 自动使用 `deepseek/`。 - `deepseek-reasoner` 映射为 `deepseek/deepseek-reasoner`。 - 测试改为不依赖本机是否安装 `aider`。 - 测试: - `node --test llm-providers.test.mjs tool-gateway.test.mjs agent-run-gateway.test.mjs` - 结果: `43 passed` - 生产发布: - release id: `20260702-094216-0cfd2d3` - git head: `0cfd2d3d2adf404d4ad7f68dcc37c8074c14e900` - 发布脚本在 legacy `m.tkmind.cn` tunnel 检查处返回 1,但 Portal runtime 已发布并通过健康检查。 - 已手动清理占用 8081 的孤儿 Portal node 进程,让 LaunchAgent 重新接管。 - 最终监听: - LaunchAgent `cn.tkmind.memind-portal` - pid: `86462` - port: `8081` 第二次真实 canary: - canary dir: - `/Users/john/Project/memind_canary/tool-gateway-p64-fixed-20260702-094739` - run id: - `7daff187-1f60-4e67-9848-a404274a0455` - request id: - `p64-real-aider-fixed-20260702014740` - status: - `succeeded` - attempts: `1` - `agent_session_id=null` - event proof: - `queued` - `running` - `tool_gateway_dispatch` - `protocol=agent-run-v1` - workingDir is the canary repo - `tool_gateway_result` - `dryRun=false` - `executor=aider` - `exitCode=0` - `succeeded` - file proof: - generated file: - `/Users/john/Project/memind_canary/tool-gateway-p64-fixed-20260702-094739/P64_CANARY.md` - content: - `P6.4 real Tool Gateway canary succeeded.` 恢复/最终状态: - 生产 `.env` 无 `MEMIND_TOOL_GATEWAY_*`。 - 生产 `.env` 无 `MEMIND_AGENT_CODE_RUNS_*`。 - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.toolGateway.enabled=false` - all workers `heartbeatSource=sidecar` - all workers `heartbeatOk=true` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` 边界: - 未做 PG schema migration。 - 仅新增 canary run/event 审计记录。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 真实 Aider 文件写入只发生在 `/Users/john/Project/memind_canary/...`。 ### 2026-07-02 P6.5 OpenHands Real Canary 目标: - 在专用 canary workspace / 空 git 仓库内验证真实 OpenHands executor。 - 使用 `taskType=repo_refactor` 触发 Tool Gateway 选择 `openhands`。 - 不打开普通生产 code run。 - 不使用生产用户真实页面。 - 不删除 PG 数据,不删除或改写 `/Users/john/Project/Memind/MindSpace` 用户数据。 执行: - OpenHands CLI: - version: `1.16.0` - SDK: `1.21.0` - launch plan: - command: `openhands` - args: - `--headless` - `--json` - `--override-with-envs` - `--task ` - env: - `LLM_MODEL=deepseek-reasoner` - `LLM_BASE_URL=https://api.deepseek.com/v1` - `LLM_API_KEY=` - canary dir: - `/Users/john/Project/memind_canary/tool-gateway-p65-openhands-20260702-095402` - run id: - `0b1bb7a0-d111-44c1-91b5-4f14b2f9c01c` - request id: - `p65-real-openhands-20260702015402` - duration: - about `206s` - status: - `succeeded` - attempts: `1` - `agent_session_id=null` event proof: - `queued` - `running` - `tool_gateway_dispatch` - `protocol=agent-run-v1` - `taskType=repo_refactor` - workingDir is the canary repo - `tool_gateway_result` - `dryRun=false` - `executor=openhands` - `exitCode=0` - `succeeded` file proof: - generated file: - `/Users/john/Project/memind_canary/tool-gateway-p65-openhands-20260702-095402/P65_OPENHANDS_CANARY.md` - content: - `P6.5 OpenHands Tool Gateway canary succeeded.` 恢复/最终状态: - 生产 `.env` 无 `MEMIND_TOOL_GATEWAY_*`。 - 生产 `.env` 无 `MEMIND_AGENT_CODE_RUNS_*`。 - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.toolGateway.enabled=false` - all workers `heartbeatSource=sidecar` - all workers `heartbeatOk=true` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` 边界: - 未做 PG schema migration。 - 仅新增 canary run/event 审计记录。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 真实 OpenHands 文件写入只发生在 `/Users/john/Project/memind_canary/...`。 ### 2026-07-02 P6.6 External Worker Code-run Canary 目标: - 验证 code-mode run 可以由 Portal 以外的 external worker 精确接管。 - 确认 Portal `autoDispatch=false` 时不会抢跑 queued run。 - 使用专用 canary workspace,不触碰生产用户页面。 - 执行后恢复生产默认关闭。 源码: - commit: - `a2d69a0 feat: target agent run worker dispatch` - 关键变更: - `scripts/agent-run-worker.mjs` 支持 `--run-id ` 精确 dispatch 单条 run。 - 支持 `MEMIND_AGENT_RUN_WORKDIR_OVERRIDE` 和 `MEMIND_AGENT_RUN_WORKDIR_USER_ID`,用于把 canary 文件写入专用目录。 - worker 输出包含 `runId`,便于审计。 生产 release: - release id: - `20260702-100212-a2d69a0` - manifest: - `git_head=a2d69a0c36f4300bc36a79abfe01300f2237fd07` - `branch=memind-streaming-runtime-20260702` 执行: - 生产 `.env` 短窗口启用: - `MEMIND_AGENT_RUN_AUTODISPATCH=0` - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - `MEMIND_AGENT_CODE_RUNS_USER_IDS=a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - 外部 worker 单次执行时启用: - `MEMIND_TOOL_GATEWAY_ENABLED=1` - `MEMIND_TOOL_GATEWAY_DRY_RUN=0` - `MEMIND_AGENT_RUN_WORKDIR_OVERRIDE=/Users/john/Project/memind_canary/tool-gateway-p66-worker-20260702-100500` - `MEMIND_AGENT_RUN_WORKDIR_USER_ID=a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - code run: - run id: `94dcca6f-cf32-4fa1-b602-8e6a5bff1e3c` - request id: `p66-worker-aider-20260702100509` worker proof: - command path: - `/Users/john/Project/Memind/scripts/agent-run-worker.mjs --run-id 94dcca6f-cf32-4fa1-b602-8e6a5bff1e3c` - output: - `ok=true` - `mode=dispatch` - `dispatched=1` - `runId=94dcca6f-cf32-4fa1-b602-8e6a5bff1e3c` - queue `autoDispatch=false` - Tool Gateway `enabled=true` - `dryRun=false` event proof: - `queued` - `running` - `tool_gateway_dispatch` - canary workingDir - `tool_gateway_result` - `dryRun=false` - `executor=aider` - `exitCode=0` - `succeeded` file proof: - generated file: - `/Users/john/Project/memind_canary/tool-gateway-p66-worker-20260702-100500/P66_WORKER_CANARY.md` - content: - `P6.6 external worker Tool Gateway canary succeeded.` 恢复/最终状态: - 已恢复 `.env`。 - 临时登录 session 已 revoke。 - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.toolGateway.enabled=false` - all workers `heartbeatSource=sidecar` - all workers `heartbeatOk=true` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` 边界: - 未做 PG schema migration。 - 仅新增 canary run/event 审计记录。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 真实文件写入只发生在 `/Users/john/Project/memind_canary/...`。 ### 2026-07-02 P6.7 Tool Gateway Production Guardrails 目标: - 解决 executor `exitCode=0` 但目标产物未生成时仍被标记 `succeeded` 的风险。 - 为 canary 和灰度 code run 增加显式成功标准。 - 保持生产默认关闭,不改变普通聊天链路。 源码: - commit: - `6340b57 feat: validate tool gateway artifacts` - branch: - `memind-streaming-runtime-20260702` 关键变更: - `agent-run-gateway.mjs` - 支持读取 `metadata.memindRun.validation` 或 `metadata.toolGatewayValidation`。 - 支持校验: - `expectedFile` - `expectedFiles` - `contains` / `expectedContent` / `contentIncludes` - 校验路径限制在 Tool Gateway working directory 内。 - `tool_gateway_result` event 增加 `stdoutTail` / `stderrTail`。 - 校验通过写入 `tool_gateway_validation` event。 - 校验失败写入 `tool_gateway_validation_failed` event。 - 校验失败 `retryable=false`,避免确定性产物缺失被重复重试。 验证: - `node --test agent-run-gateway.test.mjs tool-gateway.test.mjs` - `17 passed` - `node --check agent-run-gateway.mjs` - passed - `npm run build:portal-runtime` - passed 生产 release: - release id: - `20260702-101319-6340b57` - manifest: - `git_head=6340b57fda61122a61b0f767eee118be81b151e6` - `git_branch=memind-streaming-runtime-20260702` 发布说明: - release 脚本第一次在已知历史 docx 缺失链接处停止。 - 按用户要求不修复/不删除/不改写历史用户数据,第二次发布使用 `ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1` 仅跳过该守卫。 - release 过程中脚本完成当前 live 全目录备份和持久目录备份。 - 105 legacy tunnel step 仍由脚本执行并健康,但 H5 主路径继续是 `https://mm.tkmind.cn`。 发布后状态: - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.toolGateway.enabled=false` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` - `writes.database=false` - `writes.mindSpace=false` - `writes.redis=false` - `writes.report=false` - `writes.reportPrune=false` 边界: - 未做 PG schema migration。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 新能力默认关闭,只影响显式 Tool Gateway code run 且声明 validation 的任务。 ### 2026-07-02 P6.8 Tool Gateway Controlled Rollout 基础 目标: - 为 external agent-run-worker 准备标准 LaunchAgent 运维入口。 - 默认不启动、不接管生产队列。 - 保持 `codeRunsEnabled=false`、`toolGateway.enabled=false`。 源码: - commit: - `3654639 feat: add agent run worker launchagent installer` 关键变更: - 新增: - `scripts/install-agent-run-worker-agent.sh` - 更新: - `scripts/build-portal-runtime.mjs` - runtime artifact 会带上 installer,并在 RUNBOOK 中记录入口。 installer 默认行为: - 写入: - `/Users/john/Library/LaunchAgents/cn.tkmind.memind-agent-run-worker.plist` - 默认: - `RunAtLoad=false` - `KeepAlive=true` - `launchctl disable` - 不 `kickstart` - 默认环境: - `MEMIND_AGENT_RUN_AUTODISPATCH=0` - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1` - `MEMIND_TOOL_GATEWAY_ENABLED=0` - `MEMIND_TOOL_GATEWAY_DRY_RUN=0` 验证: - `bash -n scripts/install-agent-run-worker-agent.sh` - passed - `node --check scripts/build-portal-runtime.mjs` - passed - `node --test agent-run-gateway.test.mjs tool-gateway.test.mjs` - `17 passed` - `npm run build:portal-runtime` - passed 生产 release: - release id: - `20260702-101834-3654639` - manifest: - `git_head=3654639bb314437a7463446f47f40ab1c15aeb92` - `git_branch=memind-streaming-runtime-20260702` 生产安装状态: - installer: - `/Users/john/Project/Memind/scripts/install-agent-run-worker-agent.sh` - LaunchAgent: - `/Users/john/Library/LaunchAgents/cn.tkmind.memind-agent-run-worker.plist` - state: - `installed-disabled` - `print-disabled`: disabled - `launchctl print`: `state = not running` - no `agent-run-worker.mjs` process 发布后状态: - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.toolGateway.enabled=false` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` - writes all false 边界: - 未做 PG schema migration。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 未启动 external worker。 ### 2026-07-02 P6.9 Validated External Worker Canary 目标: - 使用 LaunchAgent 方式短窗口启动 external `agent-run-worker`。 - 强制 worker 写入专用 canary repo,不触碰生产 MindSpace 用户页面。 - 验证 Tool Gateway validation 的失败路径和成功路径。 - 完成后恢复默认关闭。 准备补丁: - commit: - `b12c1b3 feat: support worker canary workdir override` - release: - `20260702-102636-b12c1b3` - 目的: - installer 支持 `MEMIND_AGENT_RUN_WORKDIR_OVERRIDE` - installer 支持 `MEMIND_AGENT_RUN_WORKDIR_USER_ID` 短窗口配置: - `.env` backup: - `/Users/john/Project/memind_backups/20260702-1029-p69-validated-worker-canary/.env.before` - Portal: - `MEMIND_AGENT_RUN_AUTODISPATCH=0` - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - `MEMIND_AGENT_CODE_RUNS_USER_IDS=a6fb1e97-2b0f-447b-b138-4561d8e5c53e` - `MEMIND_TOOL_GATEWAY_ENABLED=0` - external worker LaunchAgent: - `MEMIND_TOOL_GATEWAY_ENABLED=1` - `MEMIND_TOOL_GATEWAY_DRY_RUN=0` - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1` - `MEMIND_AGENT_RUN_WORKDIR_OVERRIDE=/Users/john/Project/memind_canary/tool-gateway-p69-validated-worker-20260702-102942` - `MEMIND_AGENT_RUN_WORKDIR_USER_ID=a6fb1e97-2b0f-447b-b138-4561d8e5c53e` canary repo: - `/Users/john/Project/memind_canary/tool-gateway-p69-validated-worker-20260702-102942` 失败路径 proof: - run id: - `1353d917-c03b-4fb8-9acd-739d56984fc5` - request id: - `p69-validated-worker-20260702022958` - final status: - `failed` - attempts: `1` - event chain: - `queued` - `running` - `tool_gateway_dispatch` - `tool_gateway_result` - `executor=aider` - `dryRun=false` - `exitCode=0` - `tool_gateway_validation_failed` - `reason=missing_content` - `failed` - 说明: - Aider exit 0 且生成文件,但内容与 expected content 不完全匹配,validation 正确阻止 succeeded。 成功路径 proof: - run id: - `980424de-5df5-4294-9d1d-8901ae66be6f` - request id: - `p69-validated-worker-pass-20260702023132` - final status: - `succeeded` - attempts: `1` - `agent_session_id=null` - event chain: - `queued` - `running` - `tool_gateway_dispatch` - `tool_gateway_result` - `executor=aider` - `dryRun=false` - `exitCode=0` - `tool_gateway_validation` - `expectedFiles[0].path=P69_VALIDATED_WORKER_CANARY_PASS.md` - `contains=true` - `sizeBytes=31` - `succeeded` - generated file: - `/Users/john/Project/memind_canary/tool-gateway-p69-validated-worker-20260702-102942/P69_VALIDATED_WORKER_CANARY_PASS.md` - content: - `P69_VALIDATED_WORKER_CANARY_OK` 恢复/最终状态: - 已恢复 `.env`。 - 临时 login sessions 已 revoke: - `revokedRows=2` - worker LaunchAgent: - `installed-disabled` - `state=not running` - no `agent-run-worker.mjs` process - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.toolGateway.enabled=false` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` - writes all false 边界: - 未做 PG schema migration。 - 仅新增 canary run/event 审计记录和临时 login session 记录,临时 session 已 revoke。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 真实文件写入只发生在 `/Users/john/Project/memind_canary/...`。 ### 2026-07-02 P6.10 External Worker Observability 目标: - 为 external worker 增加独立只读状态检查。 - 默认期望 worker disabled/not running。 - 继续保持生产 code-run/Tool Gateway 默认关闭。 源码: - commit: - `f1fd4cc feat: add agent run worker status check` 新增: - `scripts/check-agent-run-worker.mjs` 能力: - 检查 LaunchAgent: - plist 是否存在 - `launchctl print` state - disabled/enabled 状态 - pid / program / path - `agent-run-worker.mjs` 进程列表 - 检查 queue: - queued/running/retryable counts - oldest pending age - latest failed run - 默认: - 期望 disabled/not running - 可选: - `MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=1` 时期望 running 验证: - `node --check scripts/check-agent-run-worker.mjs` - passed - `MEMIND_ENV_FILE=/Users/john/Project/Memind/.env node scripts/check-agent-run-worker.mjs` - `ok=true` - `node --test agent-run-gateway.test.mjs tool-gateway.test.mjs` - `17 passed` - `npm run build:portal-runtime` - passed 生产 release: - release id: - `20260702-103631-f1fd4cc` - manifest: - `git_head=f1fd4cc31666b33962e9a730cb7d925e860e6db5` 发布后验证: - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.toolGateway.enabled=false` - `node scripts/check-agent-run-worker.mjs`: - `ok=true` - `expected=disabled` - `installed=true` - `disabled=true` - `running=false` - `queue.statusCounts={}` - `oldestPendingAgeMs=0` - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` - writes all false 边界: - 未做 PG schema migration。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 新脚本只读。 ### 2026-07-02 P6.11 Single-user Rollout Policy 目标: - 为后续单用户长期灰度建立策略门禁。 - 默认关闭,不改变普通用户路径。 - 在 runtime/status 中可观测当前 code-run policy。 源码: - commit: - `212e163 feat: add code run rollout policy gates` 新增策略: - `MEMIND_AGENT_CODE_RUNS_ENABLED` - code run 总开关。 - `MEMIND_AGENT_CODE_RUNS_USER_IDS` - 用户级 allowlist。 - `MEMIND_AGENT_CODE_RUN_TASK_TYPES` - task type allowlist;为空表示不限制 task type。 - `MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION` - 开启后,code run 必须声明 `metadata.memindRun.validation` 或 `metadata.toolGatewayValidation`。 入口行为: - 非 allowlist 用户: - `403 当前用户未开启代码任务灰度` - 非 allowlist task type: - `403 当前代码任务类型未开启灰度` - 缺少 required validation: - `400 代码任务必须声明产物校验规则` runtime/status: - 新增: - `toolRuntime.codeRunPolicy.enabled` - `toolRuntime.codeRunPolicy.userAllowlist` - `toolRuntime.codeRunPolicy.taskTypeAllowlist` - `toolRuntime.codeRunPolicy.requireValidation` 验证: - `node --test agent-run-routes.test.mjs agent-run-gateway.test.mjs tool-gateway.test.mjs` - `31 passed` - `node --check agent-run-routes.mjs` - passed - `node --check server.mjs` - passed - `npm run build:portal-runtime` - passed 生产 release: - release id: - `20260702-104734-212e163` - manifest: - `git_head=212e163006ae120827923335e568845a886db775` 发布备注: - release 脚本在 legacy `m.tkmind.cn` tunnel step 短暂返回失败;Portal runtime 已切换且健康。 - 后续检查显示: - `mm.tkmind.cn` 主路径正常。 - `cn.tkmind.memind-portal-tunnel` 已恢复 running。 - Portal node pid `35983` 正在监听 `:8081`。 - 该 legacy tunnel 不是当前 H5 主路径,当前 H5 主路径仍是 `https://mm.tkmind.cn`。 发布后状态: - `https://mm.tkmind.cn/api/runtime/status`: - `ok=true` - `toolRuntime.codeRunPolicy.enabled=false` - `toolRuntime.codeRunPolicy.userAllowlist=[]` - `toolRuntime.codeRunPolicy.taskTypeAllowlist=[]` - `toolRuntime.codeRunPolicy.requireValidation=false` - `toolRuntime.codeRunsEnabled=false` - `toolRuntime.queue.autoDispatch=true` - `toolRuntime.queue.toolGateway.enabled=false` - `node scripts/check-agent-run-worker.mjs`: - `ok=true` - worker disabled/not running - `scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` - writes all false 边界: - 未做 PG schema migration。 - 未删除 PG 数据。 - 未删除或改写生产 MindSpace 用户数据。 - 默认关闭,不开启真实用户 code run。 ### 2026-07-02 P6.12 All-user Long-running Gray 目标: - 将 code-run 灰度从单用户策略门禁推进到所有 H5 用户默认开启。 - 保持 Portal 不直接执行 Tool Gateway,统一由 external worker 接管。 - 使用真实普通用户路径验证,而不是只做后台 canary。 生产变更: - Portal `.env`: - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - `MEMIND_AGENT_CODE_RUNS_USER_IDS=`,空值表示不限制用户。 - `MEMIND_AGENT_CODE_RUN_TASK_TYPES=`,空值表示不限制任务类型。 - `MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION=0`,H5 默认 code-run 不强制声明 expected file validation;带 validation 的任务仍走 Tool Gateway 校验。 - `MEMIND_AGENT_RUN_AUTODISPATCH=0`,Portal 不内联派发 code-run。 - `MEMIND_TOOL_GATEWAY_ENABLED=0`,Portal 侧 Tool Gateway 保持关闭。 - H5 前端重建: - `VITE_AGENT_CODE_RUNS_ENABLED=1` - `VITE_AGENT_CODE_RUNS_AUTODETECT=1` - 当前公网 bundle: `/assets/index-CI7mXNQo.js` - external worker: - `cn.tkmind.memind-agent-run-worker` enabled/running。 - `MEMIND_TOOL_GATEWAY_ENABLED=1` - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1` 备份: - `/Users/john/Project/memind_backups/20260702-111654-all-user-code-run-gray` 真实用户验证: - 测试用户: - `gray20260702112309` - user id `b4561354-4273-4900-9ada-b6c52d2c5836` - run: - `aa0d1ac1-c03b-4110-baea-6cac83556567` - request id `gray-code-20260702112309-login` - status `succeeded` - attempts `1` - 事件链: - `queued` - `running` - `tool_gateway_dispatch` - `tool_gateway_result` - `succeeded` - executor: - `aider` - `dryRun=false` - 工作目录: - `/Users/john/Project/Memind/MindSpace/b4561354-4273-4900-9ada-b6c52d2c5836` - 产物: - `/Users/john/Project/Memind/MindSpace/b4561354-4273-4900-9ada-b6c52d2c5836/public/gray-code-run-20260702112309.html` - 内容匹配 `GRAY_CODE_RUN_20260702112309` 生产观察: - `MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=1 node scripts/check-agent-run-worker.mjs`: - `ok=true` - worker `running=true` - queue `statusCounts={}` - `oldestPendingAgeMs=0` - `node scripts/runtime-slo-report.mjs`: - `ok=true` - `failures=[]` 边界: - 未做 PG schema migration。 - 未删除 PG 数据。 - 未删除或改写既有生产 MindSpace 用户数据。 - 真实测试产物仅写入新建测试用户自己的 MindSpace。 ### 2026-07-02 P6.13 Agent Run Auto-pause Guard 目标: - 全用户灰度开启后,增加自动暂停保护。 - 当失败或队列积压超过阈值时,自动关闭 code-run gate 并停掉 external worker,防止故障扩散。 新增源码: - `scripts/agent-run-guard.mjs` - `scripts/install-agent-run-guard-agent.sh` - `docs/agent-run-worker-rollout-runbook.md` - `scripts/build-portal-runtime.mjs` 已同步 runtime 打包清单。 生产安装: - LaunchAgent: - label `cn.tkmind.memind-agent-run-guard` - interval `60s` - log `/Users/john/Library/Logs/memind-agent-run-guard.log` - 安装命令: - `bash /Users/john/Project/Memind/scripts/install-agent-run-guard-agent.sh` 默认触发阈值: - 10 分钟内 failed run 数 `>= 3` - 最老 queued/retryable age `>= 300000ms` - queued + retryable 数量 `>= 10` - 最老 running age `>= 900000ms` 触发动作: - 备份 `.env` 到 `/Users/john/Project/memind_backups/-agent-run-guard-pause/.env.before` - 写入: - `MEMIND_AGENT_CODE_RUNS_ENABLED=0` - `MEMIND_AGENT_RUN_AUTODISPATCH=1` - `launchctl bootout/disable cn.tkmind.memind-agent-run-worker` - `launchctl kickstart -k cn.tkmind.memind-portal` 验证: - `node scripts/agent-run-guard.mjs --dry-run`: - `ok=true` - `shouldPause=false` - queue empty - recent failed count `0` - `launchctl print gui/$(id -u)/cn.tkmind.memind-agent-run-guard`: - enabled - `run interval = 60 seconds` - last exit code `0` - 安装后复查: - worker 仍 running - code-run all-user gate 仍 enabled - SLO `ok=true`, `failures=[]` 备份: - `/Users/john/Project/memind_backups/20260702-113106-agent-run-guard-install` ### 2026-07-02 P6.15 H5 Code-run Validation Metadata 目标: - 在 P6.14 灰度稳定后,把 H5 默认 code-run 从“允许无 validation”升级为“自动带 validation”。 - 恢复后端 required validation gate,避免全用户灰度下出现无产物校验的 code-run。 - 不猜测用户任务真正产物;统一要求每次 code-run 额外写入可验证 receipt。 新增行为: - H5 code-run 创建时自动生成: - receipt path: `.memind/agent-runs/.json` - validation: `metadata.memindRun.validation.expectedFile` - contains: 当前 `requestId` - H5 会把简短校验指令追加到 agent-visible message: - 要求执行器创建/更新 receipt JSON。 - receipt 必须包含当前 requestId 值。 - `createAgentRun()` 在提交 `/api/agent/runs` 前合并 validation metadata。 - 普通 chat mode 不附加 receipt validation。 生产配置: - 已恢复: - `MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION=1` - 保持: - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` - `MEMIND_AGENT_CODE_RUNS_USER_IDS=` - `MEMIND_AGENT_CODE_RUN_TASK_TYPES=` - `MEMIND_AGENT_RUN_AUTODISPATCH=0` - external worker running - auto-pause guard enabled 验证: - `npm run build` 通过。 - `node --test agent-run-routes.test.mjs agent-run-gateway.test.mjs` 通过,27 tests pass。 - 生产 `/api/runtime/status`: - `codeRunPolicy.enabled=true` - `codeRunPolicy.userAllowlist=[]` - `codeRunPolicy.taskTypeAllowlist=[]` - `codeRunPolicy.requireValidation=true` - 普通测试用户真实路径通过: - user id `52ff79cf-21b2-4185-94d6-f24555126ceb` - run id `5eb28014-b376-4a0c-b42a-8dd38f781f4f` - request id `p615-validation-20260702034524` - status `succeeded` - attempts `1` - 产物验证: - receipt: `/Users/john/Project/Memind/MindSpace/52ff79cf-21b2-4185-94d6-f24555126ceb/.memind/agent-runs/p615-validation-20260702034524.json` - product: `/Users/john/Project/Memind/MindSpace/52ff79cf-21b2-4185-94d6-f24555126ceb/public/p615-validation-20260702034524.html` - event chain 包含 `tool_gateway_validation`。 - SLO: - `ok=true` - `failures=[]` - queue empty - guard `shouldPause=false` 备份: - `/Users/john/Project/memind_backups/20260702-114335-p615-validation-metadata` - worker KeepAlive 运维补强备份: `/Users/john/Project/memind_backups/20260702-115330-worker-keepalive` ### 2026-07-02 P6.17 Controlled Worker Concurrency Canary 目标: - 在 required validation 和 auto-pause guard 已稳定后,短窗口验证 external worker 并发 2。 - 不扩大 code-run gate,不改用户 allowlist,不改 task type allowlist。 - 真实 canary 使用两个新测试用户,避免同一 MindSpace/git 工作目录互相锁住。 生产变更: - 备份: - `/Users/john/Project/memind_backups/20260702-120520-p617-concurrency-canary` - worker LaunchAgent 临时提升: - `MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=2` - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=2` - `--limit 2` - `KeepAlive=true` 验证: - 两条真实 code-run 同时进入 running: - run `d064c8c0-b061-4df7-8c9b-5dfa315271ea` - user `916f1fe6-e9c8-44b5-9faa-2fc1bf146ab9` - request `p617-concurrency-20260702040600-1` - started `1782965163186` - completed `1782965180346` - status `succeeded`, attempts `1` - run `64717bcc-4065-4947-a0d3-a706f3b62640` - user `07f29934-8cf8-408a-af92-3c4276e579c8` - request `p617-concurrency-20260702040600-2` - started `1782965163135` - completed `1782965186566` - status `succeeded`, attempts `1` - 两条 run 都写入: - `tool_gateway_dispatch` - `tool_gateway_result` - `tool_gateway_validation` - `succeeded` - MindSpace 验证: - 两个 receipt JSON 均包含 requestId。 - 两个 `public/p617-concurrency-*.html` 均包含目标 marker。 - worker log: - `limit=2` - `maxConcurrentRuns=2` - `dispatched=2` - P6.17 后复查: - worker running - queue empty - guard `shouldPause=false` - SLO `ok=true`, `failures=[]` ### 2026-07-02 P6.18 Concurrency Rollout Policy 目标: - 将 P6.17 的并发 2 从短窗口 canary 固化为当前 all-user gray 的可运营策略。 - 让 worker LaunchAgent、Portal `.env` 和 runtime/status 对齐,避免状态面显示并发 1 但实际 worker 并发 2。 生产配置: - `/Users/john/Project/Memind/.env`: - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=2` - `MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=2` - worker LaunchAgent: - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=2` - `MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=2` - `--limit 2` - `KeepAlive=true` 验证: - `https://mm.tkmind.cn/api/status` 返回 `ok`。 - `/api/runtime/status`: - `codeRunPolicy.requireValidation=true` - `toolRuntime.queue.maxConcurrentRuns=2` - `toolRuntime.queue.statusCounts={}` - worker check: - running `true` - process args include `--limit 2` - guard dry-run: - `ok=true` - `shouldPause=false` - queue empty - SLO: - `ok=true` - `failures=[]` 观察: - Portal 在一次重启窗口中出现 `read EADDRNOTAVAIL` 并由 LaunchAgent 自动拉起,随后 `/api/status` 和 `/api/runtime/status` 恢复 `ok`。 - 后续建议优先做 running/stuck recovery 与 DB/session auth 异常不致命化,避免瞬时 DB/socket 错误扩大为进程退出。 ### 2026-07-02 P8.1 Queue Lease / Stuck Run Recovery 目标: - 防止 worker/进程在 run 进入 `running` 后异常退出,导致该 run 永久卡住。 - 在不新增 schema、不修改真实用户数据的前提下,先用 `started_at + runTimeoutMs` 作为最小 lease。 新增能力: - `agent-run-gateway.mjs`: - 新增 `recoverStaleRunningRuns()`。 - `dispatchQueuedRuns()` 每轮 dispatch 前自动回收超过 `runTimeoutMs` 的 `running` rows。 - stale row 会标记为 `failed`,写入 `stale_recovered` event。 - `scripts/agent-run-worker.mjs`: - 新增只读入口: - `node scripts/agent-run-worker.mjs --recover-stale --stale-ms 900000 --limit 5` - 新增应用入口: - `node scripts/agent-run-worker.mjs --recover-stale --apply-recovery --stale-ms 900000 --limit 5` - `scripts/check-agent-run-worker.mjs`: - queue 摘要增加: - `oldestRunningStartedAt` - `oldestRunningAgeMs` - runtime/status 的 queue 摘要增加 running age 字段。 测试: - `node --check agent-run-gateway.mjs scripts/agent-run-worker.mjs scripts/check-agent-run-worker.mjs` 通过。 - `node --test agent-run-gateway.test.mjs agent-run-routes.test.mjs` 通过,29 tests pass。 - 新增测试: - dry-run reports stale running rows without mutating them。 - apply marks stale running rows failed and writes `stale_recovered` event。 生产部署: - 备份: - `/Users/john/Project/memind_backups/20260702-120900-p81-stale-recovery` - 已部署: - bundled `server.mjs` - bundled `scripts/agent-run-worker.mjs` - `scripts/check-agent-run-worker.mjs` - `RUNBOOK.txt` - 已重启: - `cn.tkmind.memind-portal` - `cn.tkmind.memind-agent-run-worker` 生产验证: - `/api/status` 返回 `ok`。 - `/api/runtime/status.toolRuntime.queue`: - `maxConcurrentRuns=2` - `statusCounts={}` - `oldestRunningStartedAt=null` - `oldestRunningAgeMs=0` - stale dry-run: - `considered=0` - `recovered=0` - synthetic stale apply: - run id `9e22ad3d-ad5d-42b5-9ce3-24fe7aace3f5` - request id `p81-stale-recovery-1782965482040` - recovery `considered=1`, `recovered=1` - final status `failed` - event chain includes `stale_recovered` - post-check: - worker running with `--limit 2` - queue empty - guard `shouldPause=false` - SLO `ok=true`, `failures=[]` ### 2026-07-02 P8.2 Portal DB/Auth Transient Error Hardening 目标: - 防止 session verify / auth status / API auth 中的 MySQL 或 socket 瞬时错误扩大为进程级异常。 - 将认证依赖临时不可用时的用户可见结果收敛为 503,而不是 uncaught rejection 或 Portal 退出。 - 保持 plaza public read 路径在 session attach 瞬时失败时仍可继续读取。 改动: - `attachUserSession()`: - 捕获 `userAuth.verify(token)` 异常。 - 写入 `req.userSessionError`,记录 `[Auth] session verify failed`。 - 不让异常穿透 Express middleware。 - `/auth/status`: - 捕获 `getMe()`、`getUserById()`、`resolveUserCapabilities()` 异常。 - 返回 `503 { authenticated:false, mode:'unavailable' }`。 - API auth middleware: - 对 `req.userSessionError` 返回 503。 - 对 public plaza read 继续放行。 - 捕获后续 `getMe()` 异常并返回 503。 测试: - `node --check server.mjs` 通过。 - `node --test auth.test.mjs user-auth.test.mjs admin-guard.test.mjs` 通过,23 tests pass。 生产部署: - 备份: - `/Users/john/Project/memind_backups/20260702-121438-p82-auth-transient-hardening` - 已部署: - bundled `server.mjs` - 已重启: - `cn.tkmind.memind-portal` 生产验证: - `https://mm.tkmind.cn/api/status` 返回 `ok`。 - `https://mm.tkmind.cn/auth/status` 未登录返回 `200 {"authenticated":false,"mode":"user"}`。 - `/api/runtime/status.toolRuntime`: - `codeRunsEnabled=true` - `requireValidation=true` - `maxConcurrentRuns=2` - `oldestRunningStartedAt=null` - `oldestRunningAgeMs=0` - worker LaunchAgent running: - `--limit 2` - `MEMIND_TOOL_GATEWAY_ENABLED=1` - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=2` - guard dry-run: - `ok=true` - `shouldPause=false` - SLO: - `ok=true` - `failures=[]` ### 2026-07-02 P6.19 Task-level Artifact Validation 目标: - receipt validation 继续作为所有 code-run 的兜底。 - 对能从用户指令或 MindSpace 上下文稳定推断的具体产物追加 task-level validation。 - 页面编辑类任务即使通过 API/patch 修改页面,也需要生成带 `pageId` 的 task receipt,证明执行器处理的是目标页面。 改动: - `src/utils/agentRunMode.ts`: - 新增 `extractAgentRunPublicHtmlPaths()`,可从用户输入、`MindSpace/.../public/*.html` 和公开链接中提取 workspace-relative `public/*.html`。 - 新增 `buildAgentRunTaskValidation()`。 - `resolveAgentRunOptions()` 现在会合并: - 默认 `.memind/agent-runs/.json` receipt。 - 推断出的 `public/*.html` expectedFiles。 - `page_edit_code_task` 的 `.memind/agent-runs/-page-edit.json`,内容必须包含 `pageId`。 - `src/hooks/useTKMindChat.ts`: - 普通 H5 code-run 调用 `resolveAgentRunOptions()` 时传入 MindSpace context。 - `src/hooks/usePageEditSubChat.ts`: - 页面编辑子聊天传入 MindSpace context、`pageId` 和 `pageTitle`。 测试: - `npm run build` 通过。 - `node --test agent-run-gateway.test.mjs agent-run-routes.test.mjs` 通过,29 tests pass。 - `npx tsc --noEmit` 仍被既有历史 TS 问题阻断,本次相关 `waitForAgentRun()` 返回类型已收紧;最终以 Vite build 和 agent-run route/gateway tests 为准。 生产部署: - 使用灰度 env 重建 H5: - `VITE_AGENT_CODE_RUNS_ENABLED=1` - `VITE_AGENT_CODE_RUNS_AUTODETECT=1` - `VITE_AGENT_CODE_RUNS_USER_IDS=` - 备份: - `/Users/john/Project/memind_backups/20260702-122737-p619-task-artifact-validation` - 已同步: - `/Users/john/Project/Memind/dist` 生产验证: - `https://mm.tkmind.cn/` 已加载新 bundle: - `/assets/index-jYf9W5vk.js` - `/api/runtime/status`: - `ok=true` - `codeRunsEnabled=true` - `requireValidation=true` - `maxConcurrentRuns=2` - `statusCounts={}` - guard dry-run: - `ok=true` - `shouldPause=false` - `failedRecentCount=0` - real canary: - user `916f1fe6-e9c8-44b5-9faa-2fc1bf146ab9` - run `60067d2f-d2af-40d1-bf35-7976309bd8da` - request `p619-task-validation-20260702042911` - status `succeeded` - attempts `1` - event chain includes `tool_gateway_validation` - validated expectedFiles: - `.memind/agent-runs/p619-task-validation-20260702042911.json` - `public/p619-task-validation-20260702042911.html` - `.memind/agent-runs/p619-task-validation-20260702042911-page-edit.json` ### 2026-07-02 P8.3 Worker Lease Heartbeat 目标: - 将 running run 的 lease 从单纯 `started_at + timeout` 升级为 heartbeat-aware。 - 避免长任务仍在执行时被 stale recovery 误杀。 - 让 runtime/status、worker check 和 auto-pause guard 能看到 running heartbeat age。 改动: - `agent-run-gateway.mjs`: - 新增 `MEMIND_AGENT_RUN_HEARTBEAT_MS`,默认 `30000`。 - run 被 claim 为 `running` 后立即写入 `worker_heartbeat` event,后续按 heartbeat interval 定时写入。 - run 进入 terminal 或失败后停止 heartbeat timer。 - `recoverStaleRunningRuns()` 使用 `COALESCE(latest worker_heartbeat, started_at)` 判断 stale。 - recovery UPDATE 也复核最新 heartbeat,避免 select/update 之间误杀刚刷新 heartbeat 的 run。 - `getQueueStatus()` 增加: - `heartbeatMs` - `oldestRunningHeartbeatAt` - `oldestRunningHeartbeatAgeMs` - `runningWithoutHeartbeatCount` - `latestRunningRun` - `scripts/agent-run-guard.mjs`: - auto-pause 的 running age 判断改为 heartbeat age。 - queue 摘要增加 heartbeat 字段。 - `scripts/check-agent-run-worker.mjs`: - queue 摘要增加 heartbeat 字段。 - `.env.example` 和 worker rollout runbook: - 记录 `MEMIND_AGENT_RUN_HEARTBEAT_MS`。 - all-user gray 下记录 `MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=1`。 测试: - `node --check agent-run-gateway.mjs scripts/agent-run-worker.mjs scripts/agent-run-guard.mjs scripts/check-agent-run-worker.mjs` 通过。 - `node --test agent-run-gateway.test.mjs agent-run-routes.test.mjs` 通过,31 tests pass。 - 新增测试: - worker dispatch 会写 `worker_heartbeat`。 - stale recovery 不回收有新 heartbeat 的旧 running run。 - queue status 展示 heartbeat age 与 missing heartbeat count。 生产部署: - 备份: - `/Users/john/Project/memind_backups/20260702-123510-p83-worker-heartbeat` - 已部署: - bundled `server.mjs` - bundled `scripts/agent-run-worker.mjs` - `scripts/agent-run-guard.mjs` - `scripts/check-agent-run-worker.mjs` - 已重启: - `cn.tkmind.memind-portal` - `cn.tkmind.memind-agent-run-worker` - 生产 `.env` 追加: - `MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=1` 生产验证: - `https://mm.tkmind.cn/api/status` 返回 `ok`。 - `/api/runtime/status.toolRuntime.queue`: - `maxConcurrentRuns=2` - `heartbeatMs=30000` - `statusCounts={}` - `oldestRunningHeartbeatAgeMs=0` - `runningWithoutHeartbeatCount=0` - `check-agent-run-worker.mjs`: - `ok=true` - `expected=running` - `running=true` - guard dry-run: - `ok=true` - `shouldPause=false` - synthetic recovery canary: - no-heartbeat run `042276ae-ecac-4b7c-9c53-c02154ea85cd` - request `p83-no-heartbeat-20260702043604` - recovery `considered=1`, `recovered=1` - stale event includes `heartbeatAt=null` and `heartbeatAgeMs` - fresh-heartbeat synthetic run `97a2ac16-0234-483d-be4f-836ba95a91e7` was not recovered and then deleted to avoid queue residue. - real worker heartbeat canary: - run `ad2894a7-dc02-49c6-a859-da6af95cb143` - request `p83-heartbeat-real-20260702043642` - status `succeeded` - event chain includes `worker_heartbeat` before `tool_gateway_dispatch` - heartbeat data includes `pid=31341`, `attempt=1`, `heartbeatMs=30000` - `tool_gateway_validation` passed for receipt and `public/p83-heartbeat-real-20260702043642.html` ### 2026-07-02 P8.4 Long-running Gray Soak and Heartbeat SLO 目标: - 不扩大并发、不做 UI,先把 P8.3 heartbeat 纳入每日 SLO 报告。 - 让 23:55 定时日报和手动 `runtime-slo-report.mjs` 都能暴露 running heartbeat 风险。 - 为 24h all-user gray soak 提供稳定观测字段和小时级留存证据。 改动: - `scripts/runtime-slo-report.mjs`: - `runtime.toolQueueSlo` 新增 heartbeat 摘要: - `heartbeatMs` - `maxConcurrentRuns` - `statusCounts` - `oldestRunningStartedAt` - `oldestRunningAgeMs` - `oldestRunningHeartbeatAt` - `oldestRunningHeartbeatAgeMs` - `runningWithoutHeartbeatCount` - `latestRunningRun` - failure 条件新增: - `agent_run_heartbeat_stale_`: running heartbeat age 超过阈值。 - `agent_run_missing_heartbeat_`: running run 长时间缺失 heartbeat。 - Markdown 报告的 Tool Queue 段落展示: - `heartbeatMs` - `oldestRunningHeartbeatAgeMs` - `runningWithoutHeartbeatCount` - `.env.example`: - 记录 `MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS=90000`。 - `docs/agent-run-worker-rollout-runbook.md`: - 记录 heartbeat SLO 检查命令和期望字段。 - `scripts/install-runtime-slo-soak-agent.sh`: - 新增独立 LaunchAgent installer。 - 默认 label `cn.tkmind.memind-runtime-slo-soak`。 - 默认 `StartInterval=3600`。 - 默认写入 `reports/runtime-slo-soak`。 - 默认保留 `7` 天。 - `scripts/build-portal-runtime.mjs`: - runtime artifact 会同步并 chmod soak installer。 测试: - `bash -n scripts/install-runtime-slo-soak-agent.sh` 通过。 - `node --check scripts/runtime-slo-report.mjs scripts/build-portal-runtime.mjs` 通过。 - 源码目录只读 SLO: - `ok=true` - `failures=[]` - `toolQueueSlo.heartbeatMs=30000` - `toolQueueSlo.runningWithoutHeartbeatCount=0` 生产部署: - 备份: - `/Users/john/Project/memind_backups/20260702-123947-p84-heartbeat-slo` - `/Users/john/Project/memind_backups/20260702-124520-p84-hourly-soak` - 已部署: - `scripts/runtime-slo-report.mjs` - `scripts/install-runtime-slo-soak-agent.sh` - 未重启 Portal、worker、goosed 或 H5。 - 现有 SLO LaunchAgent 保持: - `cn.tkmind.memind-runtime-slo-report` - 每日 `23:55` - `--write-report --prune --retention-days 30` - 新增 soak LaunchAgent: - `cn.tkmind.memind-runtime-slo-soak` - `StartInterval=3600` - `--write-report --prune --retention-days 7` - report dir `/Users/john/Project/Memind/reports/runtime-slo-soak` 生产验证: - 生产只读 SLO: - `ok=true` - `failures=[]` - `toolQueueSlo.heartbeatMs=30000` - `toolQueueSlo.statusCounts={}` - `toolQueueSlo.oldestRunningHeartbeatAgeMs=0` - `toolQueueSlo.runningWithoutHeartbeatCount=0` - 临时 report-dir 写入验证: - JSON 和 Markdown 均生成成功。 - 两种格式均包含 `heartbeatMs`、`oldestRunningHeartbeatAgeMs`、`runningWithoutHeartbeatCount`。 - soak 快照验证: - LaunchAgent 已加载。 - 首次 kickstart 快照已写入 JSON/Markdown。 - 最新快照 `ok=true`, `failures=[]`。 - JSON/Markdown 均包含 heartbeat summary。 ## 回滚策略 - P0: 修改前保留 `server.mjs` 备份;如启动失败,恢复备份并 `launchctl kickstart` Portal。 - P1: 修改 nginx 前备份 conf;`nginx -t` 成功后再 reload。 - P2: Redis Router 默认可通过 env 开关退回当前 `pickTarget()` round-robin。 - P4: Tool Gateway 默认关闭,通过用户策略逐步放量。