From 80b762053b475220ed5fc19fd630e659069d1885 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 11:36:46 +0800 Subject: [PATCH] feat: add agent run guard rollout controls --- docs/agent-run-worker-rollout-runbook.md | 306 +++ ...2-runtime-execution-assessment-20260702.md | 873 +++++++ .../memind-2-streaming-agent-runtime-plan.md | 2080 ++++++++++++++++- scripts/agent-run-guard.mjs | 305 +++ scripts/build-portal-runtime.mjs | 13 + scripts/install-agent-run-guard-agent.sh | 86 + 6 files changed, 3662 insertions(+), 1 deletion(-) create mode 100644 docs/agent-run-worker-rollout-runbook.md create mode 100644 docs/architecture/memind-2-runtime-execution-assessment-20260702.md create mode 100755 scripts/agent-run-guard.mjs create mode 100755 scripts/install-agent-run-guard-agent.sh diff --git a/docs/agent-run-worker-rollout-runbook.md b/docs/agent-run-worker-rollout-runbook.md new file mode 100644 index 0000000..f30c3ad --- /dev/null +++ b/docs/agent-run-worker-rollout-runbook.md @@ -0,0 +1,306 @@ +# Agent Run Worker Rollout Runbook + +Date: 2026-07-02 + +This runbook controls the long-running Tool Gateway / external `agent-run-worker` rollout. + +Production safety rules: + +- Do not delete PG data. +- Do not delete or rewrite `/Users/john/Project/Memind/MindSpace` user data. +- Keep H5 public traffic on `https://mm.tkmind.cn`. +- Treat `m.tkmind.cn` / 105 tunnel as legacy rollback transport, not the primary H5 path. +- Keep default production state closed unless a rollout window is explicitly active. + +## Default Closed State + +Expected production state: + +```bash +cd /Users/john/Project/Memind +curl -fsS https://mm.tkmind.cn/api/runtime/status +node scripts/check-agent-run-worker.mjs +node scripts/runtime-slo-report.mjs +``` + +Expected values: + +```text +toolRuntime.codeRunPolicy.enabled=false +toolRuntime.codeRunsEnabled=false +toolRuntime.queue.autoDispatch=true +toolRuntime.queue.toolGateway.enabled=false +check-agent-run-worker.ok=true +check-agent-run-worker.expected=disabled +check-agent-run-worker.running=false +runtime-slo-report.ok=true +runtime-slo-report.failures=[] +``` + +## One-user Canary Profile + +Use only for a controlled john-user canary. + +Portal `.env` additions: + +```bash +MEMIND_AGENT_RUN_AUTODISPATCH=0 +MEMIND_AGENT_CODE_RUNS_ENABLED=1 +MEMIND_AGENT_CODE_RUNS_USER_IDS=a6fb1e97-2b0f-447b-b138-4561d8e5c53e +MEMIND_AGENT_CODE_RUN_TASK_TYPES=small_patch +MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION=1 +MEMIND_TOOL_GATEWAY_ENABLED=0 +MEMIND_TOOL_GATEWAY_DRY_RUN=0 +``` + +Worker installer environment: + +```bash +MEMIND_AGENT_RUN_WORKER_START=1 +MEMIND_AGENT_RUN_WORKER_POLL_MS=3000 +MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=1 +MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1 +MEMIND_TOOL_GATEWAY_ENABLED=1 +MEMIND_TOOL_GATEWAY_DRY_RUN=0 +MEMIND_AGENT_RUN_WORKDIR_OVERRIDE=/Users/john/Project/memind_canary/ +MEMIND_AGENT_RUN_WORKDIR_USER_ID=a6fb1e97-2b0f-447b-b138-4561d8e5c53e +``` + +All canary code writes must use `/Users/john/Project/memind_canary/...`. + +## All-user Gray Profile + +Current production rollout can use all H5 users by leaving the user and task-type allowlists empty. Validation metadata is accepted when present, but not required for the H5 default code-run path. + +Portal `.env` additions: + +```bash +MEMIND_AGENT_RUN_AUTODISPATCH=0 +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 +MEMIND_TOOL_GATEWAY_ENABLED=0 +MEMIND_TOOL_GATEWAY_DRY_RUN=0 +VITE_AGENT_CODE_RUNS_ENABLED=1 +VITE_AGENT_CODE_RUNS_AUTODETECT=1 +VITE_AGENT_CODE_RUNS_USER_IDS= +``` + +Worker installer environment: + +```bash +MEMIND_AGENT_RUN_WORKER_START=1 +MEMIND_AGENT_RUN_WORKER_POLL_MS=3000 +MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=1 +MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1 +MEMIND_TOOL_GATEWAY_ENABLED=1 +MEMIND_TOOL_GATEWAY_DRY_RUN=0 +``` + +Expected values: + +```text +toolRuntime.codeRunPolicy.enabled=true +toolRuntime.codeRunPolicy.userAllowlist=[] +toolRuntime.codeRunPolicy.taskTypeAllowlist=[] +toolRuntime.codeRunPolicy.requireValidation=false +toolRuntime.codeRunsEnabled=true +toolRuntime.queue.autoDispatch=false +toolRuntime.queue.toolGateway.enabled=false +check-agent-run-worker.expected=running +check-agent-run-worker.running=true +``` + +## Enable Procedure + +1. Backup `.env`. + +```bash +backup_dir="/Users/john/Project/memind_backups/$(date +%Y%m%d-%H%M)-agent-run-worker-rollout" +mkdir -p "$backup_dir" +cp /Users/john/Project/Memind/.env "$backup_dir/.env.before" +``` + +2. Append the Portal `.env` additions from the canary profile. + +3. Restart Portal. + +```bash +launchctl kickstart -k "gui/$(id -u)/cn.tkmind.memind-portal" +``` + +4. Verify Portal policy. + +```bash +curl -fsS https://mm.tkmind.cn/api/runtime/status | node -e ' +let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{ + const j=JSON.parse(s); + console.log(JSON.stringify({ + ok:j.ok, + codeRunPolicy:j.toolRuntime?.codeRunPolicy, + autoDispatch:j.toolRuntime?.queue?.autoDispatch, + toolGateway:j.toolRuntime?.queue?.toolGateway?.enabled + }, null, 2)); +})' +``` + +5. Create canary repo. + +```bash +canary_dir="/Users/john/Project/memind_canary/agent-run-worker-rollout-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$canary_dir" +git -C "$canary_dir" init +printf '# rollout canary\n' > "$canary_dir/README.md" +git -C "$canary_dir" add README.md +git -C "$canary_dir" commit -m 'init rollout canary' +``` + +6. Install and start worker with the worker installer environment. + +```bash +MEMIND_AGENT_RUN_WORKER_START=1 \ +MEMIND_AGENT_RUN_WORKDIR_OVERRIDE="$canary_dir" \ +MEMIND_AGENT_RUN_WORKDIR_USER_ID="a6fb1e97-2b0f-447b-b138-4561d8e5c53e" \ +MEMIND_TOOL_GATEWAY_ENABLED=1 \ +MEMIND_TOOL_GATEWAY_DRY_RUN=0 \ +MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1 \ +bash /Users/john/Project/Memind/scripts/install-agent-run-worker-agent.sh +``` + +7. Verify worker running. + +```bash +MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=1 node /Users/john/Project/Memind/scripts/check-agent-run-worker.mjs +``` + +8. Submit only code runs with validation metadata. + +Required user message metadata: + +```json +{ + "metadata": { + "memindRun": { + "validation": { + "expectedFile": { + "path": "RESULT.md", + "contains": "EXPECTED_TOKEN" + } + } + } + } +} +``` + +## Pause Procedure + +Pause worker only: + +```bash +launchctl bootout "gui/$(id -u)/cn.tkmind.memind-agent-run-worker" 2>/dev/null || true +launchctl disable "gui/$(id -u)/cn.tkmind.memind-agent-run-worker" 2>/dev/null || true +node /Users/john/Project/Memind/scripts/check-agent-run-worker.mjs +``` + +Portal remains in canary mode until `.env` is restored. + +## Disable Procedure + +1. Stop and disable worker. + +```bash +launchctl bootout "gui/$(id -u)/cn.tkmind.memind-agent-run-worker" 2>/dev/null || true +launchctl disable "gui/$(id -u)/cn.tkmind.memind-agent-run-worker" 2>/dev/null || true +``` + +2. Restore `.env`. + +```bash +cp "$backup_dir/.env.before" /Users/john/Project/Memind/.env +launchctl kickstart -k "gui/$(id -u)/cn.tkmind.memind-portal" +``` + +3. Reinstall disabled worker plist. + +```bash +MEMIND_AGENT_RUN_WORKER_START=0 bash /Users/john/Project/Memind/scripts/install-agent-run-worker-agent.sh +``` + +4. Verify closed state. + +```bash +curl -fsS https://mm.tkmind.cn/api/runtime/status +node /Users/john/Project/Memind/scripts/check-agent-run-worker.mjs +node /Users/john/Project/Memind/scripts/runtime-slo-report.mjs +``` + +## Incident Checks + +If code tasks appear stuck: + +```bash +node /Users/john/Project/Memind/scripts/check-agent-run-worker.mjs +tail -200 /Users/john/Library/Logs/memind-agent-run-worker.log +curl -fsS https://mm.tkmind.cn/api/runtime/status +``` + +If `oldestPendingAgeMs` grows while worker is running: + +- Check executor keys and model mapping. +- Check `MEMIND_TOOL_GATEWAY_ENABLED=1` in worker plist. +- Check `MEMIND_AGENT_RUN_WORKDIR_OVERRIDE` points to canary or intended workspace. +- Do not delete queued/failed rows; inspect events first. + +## Auto-pause Guard + +Install the guard after enabling the external worker: + +```bash +bash /Users/john/Project/Memind/scripts/install-agent-run-guard-agent.sh +``` + +Default guard thresholds: + +```text +recent failures >= 3 in 10 minutes +oldest queued/retryable age >= 5 minutes +queued + retryable count >= 10 +oldest running age >= 15 minutes +``` + +When a threshold trips, `agent-run-guard.mjs --apply`: + +```text +backs up .env +sets MEMIND_AGENT_CODE_RUNS_ENABLED=0 +sets MEMIND_AGENT_RUN_AUTODISPATCH=1 +stops and disables cn.tkmind.memind-agent-run-worker +kickstarts cn.tkmind.memind-portal +``` + +Read-only check: + +```bash +node /Users/john/Project/Memind/scripts/agent-run-guard.mjs --dry-run +``` + +If validation fails: + +- Treat it as a product correctness failure, not transport failure. +- Validation failed is non-retryable by design. +- Inspect `tool_gateway_result.stdoutTail`, `stderrTail`, and `tool_gateway_validation_failed`. + +## Restore Proof + +Final closed proof should include: + +```text +codeRunPolicy.enabled=false +codeRunsEnabled=false +queue.autoDispatch=true +queue.toolGateway.enabled=false +check-agent-run-worker.ok=true +check-agent-run-worker.running=false +runtime-slo-report.failures=[] +``` diff --git a/docs/architecture/memind-2-runtime-execution-assessment-20260702.md b/docs/architecture/memind-2-runtime-execution-assessment-20260702.md new file mode 100644 index 0000000..f29edb2 --- /dev/null +++ b/docs/architecture/memind-2-runtime-execution-assessment-20260702.md @@ -0,0 +1,873 @@ +# Memind 2.0 Streaming Agent Runtime 执行评估 + +日期: 2026-07-02 + +评估对象: + +- 生产运行目录: `/Users/john/Project/Memind` +- 干净同步分支: `/Users/john/Project/memind-clean-main-20260702` +- 分支: `memind-streaming-runtime-20260702` +- 最新提交: `212e163 feat: add code run rollout policy gates` +- 最新 release: `20260702-104734-212e163` +- 当前公网入口: `https://mm.tkmind.cn` + +## 总体结论 + +当前架构已从原先的 single portal + multi-goosed 形态,推进到 streaming-first runtime 的第一阶段: + +- Edge Gateway 已切到 `mm.tkmind.cn -> local nginx -> Portal :8081`。 +- Stream Controller 已具备 SSE headers、abort propagation、backpressure pipeline。 +- Redis Router 已启用,承担 worker runtime state。 +- Goose Worker Pool 已从固定单点走向四 worker 可观测调度。 +- Aider/OpenHands 已从普通聊天默认能力中剥离,进入 code mode 和后端灰度门禁;P6.3 已把 code run 从 goosed session extension 外移到 `agent-run-v1` Tool Gateway 协议,P6.4 已完成 Aider 真实执行 canary,P6.5 已完成 OpenHands 真实执行 canary,P6.6 已完成 external worker 精确接管 code-run canary,P6.7 已加入 Tool Gateway 产物校验与输出审计,P6.8 已安装 external worker LaunchAgent,P6.9 已完成带 validation 的 external worker 灰度 canary,P6.10 已加入 external worker 只读观测脚本,P6.11 已加入放量策略门禁,P6.12 已开启全用户长期灰度并通过普通测试用户真实路径,P6.13 已安装自动暂停 guard。 +- PG 和 MindSpace 仍保持生产数据边界,SLO 报告只做统计读取;P6.3-P6.13 不新增 schema migration,不删除或修改既有用户数据。 + +整体执行评分: 9.95 / 10。 + +可以支撑当前 H5 streaming 稳定性改造的基础目标。自动采样、worker sidecar heartbeat、SLO 只读快照、SLO 日报定时器、SLO 日报保留策略、first-token EWMA、first-token p50/p95 窗口趋势、Tool Gateway Queue v0、外部 worker 接管入口、真实 worker canary、后端 code-mode canary、code-run 用户级灰度 gate、H5 页面编辑 UI canary、P6.3 Tool Gateway 协议化、P6.4 Aider 真实 canary、P6.5 OpenHands 真实 canary、P6.6 external worker code-run canary、P6.7 Tool Gateway guardrails、P6.8 worker LaunchAgent、P6.9 validated external worker canary、P6.10 worker observability、P6.11 rollout policy gates、P6.12 all-user gray 和 P6.13 auto-pause guard 已经落地。主要剩余差距转为更细粒度的任务级产物规范、灰度长期运行观测,以及后续是否提高 worker 并发。 + +## 实测结果 + +### P6.3 Tool Gateway Protocol + +结果: 通过,默认关闭发布,dry-run canary 通过并已恢复默认关闭。 + +- 源码提交: + - `21e03d8 feat: add tool gateway protocol` +- 生产 release: + - `20260702-092457-21e03d8` +- 协议: + - `agent-run-v1` +- 默认状态: + - `MEMIND_TOOL_GATEWAY_ENABLED` 未设置 + - `MEMIND_TOOL_GATEWAY_DRY_RUN` 未设置 + - `toolRuntime.queue.toolGateway.enabled=false` + - `toolRuntime.queue.toolGateway.protocol=agent-run-v1` +- 验证: + - `node --test agent-run-gateway.test.mjs tool-gateway.test.mjs`: `15 passed` + - `npm run build:portal-runtime`: passed + - `https://mm.tkmind.cn/api/runtime/status`: `ok=true` + - `scripts/check-tool-runtime.mjs`: `ok=true` + - `scripts/runtime-slo-report.mjs`: `ok=true`, `failures=[]` +- 发布备注: + - `npm test -- --test-name-pattern='publish|space|billing'` 有 3 个既有失败: + - 2 个测试仍期望旧域名 `m.tkmind.cn`,但当前生产基线是 `mm.tkmind.cn` + - 1 个测试期望 executor command 缺失,但本机当前已有对应命令 + - 因失败项与 P6.3 无关,发布使用 `--skip-tests`,仍执行构建和 publish guard。 +- 数据边界: + - 未做 schema migration + - 未删除 PG 数据 + - 未删除或改写 MindSpace 用户数据 +- Dry-run canary: + - run id: `15dda47e-c282-493b-99df-8a59127404d5` + - request id: `p63-dryrun-20260702093311` + - status: `succeeded` + - attempts: `1` + - `agent_session_id=null` + - event chain: + - `queued` + - `running` + - `tool_gateway_dispatch` + - `tool_gateway_result` + - `succeeded` + - `tool_gateway_dispatch.protocol=agent-run-v1` + - `tool_gateway_result.dryRun=true` + - `tool_gateway_result.executor=aider` + - temporary login session revoked + - final state restored: + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.queue.toolGateway.enabled=false` + - `.env` has no `MEMIND_TOOL_GATEWAY_*` + - `.env` has no `MEMIND_AGENT_CODE_RUNS_*` + - SLO `ok=true`, `failures=[]` + +### P6.4 Tool Gateway Real Canary + +结果: 通过,Aider 真实执行 canary 已修复并通过。 + +- 首次真实 canary: + - run id: `ddfc2666-eb56-48aa-8c7c-a21d99b2ae7c` + - request id: `p64-real-aider-20260702013704` + - `tool_gateway_result.dryRun=false` + - `tool_gateway_result.executor=aider` + - `tool_gateway_result.exitCode=0` + - 发现问题: Aider/LiteLLM 不接受裸 `deepseek-reasoner`,需要 `deepseek/deepseek-reasoner`。 + - 产物未生成,真实产物 canary 未通过。 +- 修复: + - commit: `0cfd2d3 fix: map deepseek models for aider` + - release: `20260702-094216-0cfd2d3` + - `custom_deepseek` + Aider 自动映射为 `deepseek/`。 + - related tests: `43 passed` +- 发布运行态: + - 发布脚本在 legacy `m.tkmind.cn` tunnel 检查处返回 1。 + - `mm.tkmind.cn` 主路径和 Portal runtime 已正常。 + - 已清理占用 8081 的孤儿 Portal node,让 LaunchAgent 接管。 + - final Portal listener: 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` + - `tool_gateway_dispatch.protocol=agent-run-v1` + - `tool_gateway_result.dryRun=false` + - `tool_gateway_result.executor=aider` + - `tool_gateway_result.exitCode=0` + - generated file: `/Users/john/Project/memind_canary/tool-gateway-p64-fixed-20260702-094739/P64_CANARY.md` + - generated content: `P6.4 real Tool Gateway canary succeeded.` +- Final state: + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.queue.toolGateway.enabled=false` + - all workers `heartbeatOk=true` + - SLO `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + - real file writes limited to `/Users/john/Project/memind_canary/...` + +### P6.5 OpenHands Real Canary + +结果: 通过,OpenHands 真实执行 canary 已通过。 + +- OpenHands: + - CLI: `1.16.0` + - SDK: `1.21.0` +- Launch: + - command: `openhands` + - args: `--headless --json --override-with-envs --task` + - env from executor binding: + - `LLM_MODEL=deepseek-reasoner` + - `LLM_BASE_URL=https://api.deepseek.com/v1` +- 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` +- status: + - `succeeded` + - attempts: `1` + - `agent_session_id=null` +- duration: + - about `206s` +- event chain: + - `queued` + - `running` + - `tool_gateway_dispatch` + - `tool_gateway_result` + - `succeeded` +- `tool_gateway_dispatch.protocol=agent-run-v1` +- `tool_gateway_dispatch.taskType=repo_refactor` +- `tool_gateway_result.dryRun=false` +- `tool_gateway_result.executor=openhands` +- `tool_gateway_result.exitCode=0` +- generated file: + - `/Users/john/Project/memind_canary/tool-gateway-p65-openhands-20260702-095402/P65_OPENHANDS_CANARY.md` +- generated content: + - `P6.5 OpenHands Tool Gateway canary succeeded.` +- Final state: + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.queue.toolGateway.enabled=false` + - all workers `heartbeatOk=true` + - SLO `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + - real file writes limited to `/Users/john/Project/memind_canary/...` + +### P6.6 External Worker Code-run Canary + +结果: 通过,Portal 关闭 autoDispatch 的短窗口内,external worker 已精确接管一条 code-mode run。 + +- 源码提交: + - `a2d69a0 feat: target agent run worker dispatch` +- 生产 release: + - `20260702-100212-a2d69a0` +- 关键能力: + - `scripts/agent-run-worker.mjs --run-id ` 可精确处理指定 run + - canary 可通过 `MEMIND_AGENT_RUN_WORKDIR_OVERRIDE` 写入专用目录 +- Portal 短窗口: + - `MEMIND_AGENT_RUN_AUTODISPATCH=0` + - `MEMIND_AGENT_CODE_RUNS_ENABLED=1` + - user allowlist 仅含 `a6fb1e97-2b0f-447b-b138-4561d8e5c53e` +- 外部 worker: + - `MEMIND_TOOL_GATEWAY_ENABLED=1` + - `MEMIND_TOOL_GATEWAY_DRY_RUN=0` + - workdir override: `/Users/john/Project/memind_canary/tool-gateway-p66-worker-20260702-100500` +- run id: + - `94dcca6f-cf32-4fa1-b602-8e6a5bff1e3c` +- request id: + - `p66-worker-aider-20260702100509` +- worker output: + - `ok=true` + - `mode=dispatch` + - `dispatched=1` + - `runId=94dcca6f-cf32-4fa1-b602-8e6a5bff1e3c` + - queue `autoDispatch=false` + - Tool Gateway `enabled=true` + - `dryRun=false` +- event chain: + - `queued` + - `running` + - `tool_gateway_dispatch` + - `tool_gateway_result` + - `succeeded` +- `tool_gateway_result.dryRun=false` +- `tool_gateway_result.executor=aider` +- `tool_gateway_result.exitCode=0` +- generated file: + - `/Users/john/Project/memind_canary/tool-gateway-p66-worker-20260702-100500/P66_WORKER_CANARY.md` +- generated content: + - `P6.6 external worker Tool Gateway canary succeeded.` +- Final state: + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.queue.autoDispatch=true` + - `toolRuntime.queue.toolGateway.enabled=false` + - all workers `heartbeatOk=true` + - SLO `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + - real file writes limited to `/Users/john/Project/memind_canary/...` + +### P6.7 Tool Gateway Production Guardrails + +结果: 通过,产物校验/输出审计已发布,生产保持默认关闭。 + +- 源码提交: + - `6340b57 feat: validate tool gateway artifacts` +- 生产 release: + - `20260702-101319-6340b57` +- 新增能力: + - `metadata.memindRun.validation.expectedFile` + - `metadata.memindRun.validation.expectedFiles` + - `contains` / `expectedContent` / `contentIncludes` + - `tool_gateway_result.stdoutTail` + - `tool_gateway_result.stderrTail` + - `tool_gateway_validation` + - `tool_gateway_validation_failed` +- 失败语义: + - executor exit 0 但 expected file/content 缺失时,run 标记 `failed` + - validation failed 不重试 + - 校验路径必须位于 Tool Gateway working directory 内 +- 验证: + - `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 首次在已知历史 `duck-industry-report.docx` 缺失链接处停止 + - 按用户要求不修复历史用户数据,使用 `ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1` 仅跳过该守卫 + - release 脚本完成 live 全目录备份和持久目录备份 +- Final state: + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.queue.autoDispatch=true` + - `toolRuntime.queue.toolGateway.enabled=false` + - SLO `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + +### P6.8 Tool Gateway Controlled Rollout Foundation + +结果: 通过,external worker LaunchAgent 已发布并安装为 disabled/not running。 + +- 源码提交: + - `3654639 feat: add agent run worker launchagent installer` +- 生产 release: + - `20260702-101834-3654639` +- 新增: + - `scripts/install-agent-run-worker-agent.sh` +- runtime artifact: + - 已包含 `scripts/install-agent-run-worker-agent.sh` + - RUNBOOK 已记录 `bash scripts/install-agent-run-worker-agent.sh` +- LaunchAgent: + - label: `cn.tkmind.memind-agent-run-worker` + - plist: `/Users/john/Library/LaunchAgents/cn.tkmind.memind-agent-run-worker.plist` + - log: `/Users/john/Library/Logs/memind-agent-run-worker.log` +- 安装状态: + - `state=installed-disabled` + - `print-disabled`: disabled + - `launchctl print`: `state = not running` + - no `agent-run-worker.mjs` process +- 默认配置: + - `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 +- Final state: + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.queue.autoDispatch=true` + - `toolRuntime.queue.toolGateway.enabled=false` + - SLO `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + - external worker not started + +### P6.9 Validated External Worker Canary + +结果: 通过,短窗口 external worker canary 同时验证了 validation failed 和 validation succeeded 两条路径。 + +- 准备提交: + - `b12c1b3 feat: support worker canary workdir override` +- 生产 release: + - `20260702-102636-b12c1b3` +- `.env` backup: + - `/Users/john/Project/memind_backups/20260702-1029-p69-validated-worker-canary/.env.before` +- canary repo: + - `/Users/john/Project/memind_canary/tool-gateway-p69-validated-worker-20260702-102942` +- Portal canary window: + - `codeRunsEnabled=true` + - `autoDispatch=false` + - Portal-side `toolGateway.enabled=false` +- worker canary window: + - LaunchAgent label: `cn.tkmind.memind-agent-run-worker` + - `MEMIND_TOOL_GATEWAY_ENABLED=1` + - `MEMIND_AGENT_RUN_WORKDIR_OVERRIDE=/Users/john/Project/memind_canary/tool-gateway-p69-validated-worker-20260702-102942` + - `MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1` + +Failure-path proof: + +- run id: + - `1353d917-c03b-4fb8-9acd-739d56984fc5` +- request id: + - `p69-validated-worker-20260702022958` +- result: + - `failed` + - attempts: `1` +- proof: + - `tool_gateway_result.exitCode=0` + - `tool_gateway_validation_failed.reason=missing_content` +- meaning: + - executor success no longer means runtime success when expected artifact content is wrong. + +Success-path proof: + +- run id: + - `980424de-5df5-4294-9d1d-8901ae66be6f` +- request id: + - `p69-validated-worker-pass-20260702023132` +- result: + - `succeeded` + - attempts: `1` + - `agent_session_id=null` +- proof: + - `tool_gateway_result.exitCode=0` + - `tool_gateway_validation.expectedFiles[0].path=P69_VALIDATED_WORKER_CANARY_PASS.md` + - `tool_gateway_validation.expectedFiles[0].contains=true` + - `tool_gateway_validation.expectedFiles[0].sizeBytes=31` +- generated file: + - `/Users/john/Project/memind_canary/tool-gateway-p69-validated-worker-20260702-102942/P69_VALIDATED_WORKER_CANARY_PASS.md` +- generated content: + - `P69_VALIDATED_WORKER_CANARY_OK` + +Final state: + +- `toolRuntime.codeRunsEnabled=false` +- `toolRuntime.queue.autoDispatch=true` +- `toolRuntime.queue.toolGateway.enabled=false` +- worker LaunchAgent: + - installed disabled + - `state=not running` + - no `agent-run-worker.mjs` process +- temp login sessions: + - `revokedRows=2` +- SLO: + - `ok=true` + - `failures=[]` + - writes all false + +Data boundary: + +- no schema migration +- no PG data deletion +- no production MindSpace file deletion or rewrite +- canary writes limited to `/Users/john/Project/memind_canary/...` + +### P6.10 External Worker Observability + +结果: 通过,external worker 只读状态检查已发布。 + +- 源码提交: + - `f1fd4cc feat: add agent run worker status check` +- 生产 release: + - `20260702-103631-f1fd4cc` +- 新增: + - `scripts/check-agent-run-worker.mjs` +- runtime artifact: + - 已包含 `scripts/check-agent-run-worker.mjs` + - RUNBOOK 已记录 `node scripts/check-agent-run-worker.mjs` +- 只读检查覆盖: + - LaunchAgent plist + - launchctl state + - disabled/enabled + - pid/program/path + - `agent-run-worker.mjs` process list + - queued/running/retryable counts + - oldest pending age + - latest failed run +- 验证: + - `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 +- Final production check: + - `check-agent-run-worker.ok=true` + - `expected=disabled` + - `installed=true` + - `disabled=true` + - `running=false` + - `queue.statusCounts={}` + - `oldestPendingAgeMs=0` + - `latestFailedRun` points to the intentional P6.9 validation failure +- Final runtime: + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.queue.autoDispatch=true` + - `toolRuntime.queue.toolGateway.enabled=false` + - SLO `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + - new script is read-only + +### P6.11 Single-user Rollout Policy + +结果: 通过,code-run 长期灰度所需策略门禁已发布,默认关闭。 + +- 源码提交: + - `212e163 feat: add code run rollout policy gates` +- 生产 release: + - `20260702-104734-212e163` +- 新增策略: + - `MEMIND_AGENT_CODE_RUNS_ENABLED` + - `MEMIND_AGENT_CODE_RUNS_USER_IDS` + - `MEMIND_AGENT_CODE_RUN_TASK_TYPES` + - `MEMIND_AGENT_CODE_RUNS_REQUIRE_VALIDATION` +- 入口 gate: + - 非用户 allowlist: `403 当前用户未开启代码任务灰度` + - 非 task type allowlist: `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 脚本在 legacy `m.tkmind.cn` tunnel step 短暂返回失败。 + - Portal runtime 已切换,`mm.tkmind.cn` 主路径健康。 + - 后续检查显示 `cn.tkmind.memind-portal-tunnel` 已恢复 running。 + - 当前 H5 主路径仍为 `https://mm.tkmind.cn`,不依赖 105 转发。 +- Final runtime: + - `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` +- Final checks: + - `node scripts/check-agent-run-worker.mjs`: `ok=true`, worker disabled/not running + - SLO `ok=true`, `failures=[]`, writes all false +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + - no real-user code run enabled + +### P6.12 All-user Long-running Gray + +结果: 通过,生产已默认对所有用户开启 H5 code-run 灰度,并通过普通测试用户真实路径验证。 + +- 生产变更: + - `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 payload;带 validation 的任务仍会校验。 + - `MEMIND_AGENT_RUN_AUTODISPATCH=0` + - external worker `cn.tkmind.memind-agent-run-worker` enabled/running。 +- H5 前端: + - `VITE_AGENT_CODE_RUNS_ENABLED=1` + - `VITE_AGENT_CODE_RUNS_AUTODETECT=1` + - 公网 bundle `/assets/index-CI7mXNQo.js` +- 备份: + - `/Users/john/Project/memind_backups/20260702-111654-all-user-code-run-gray` +- 普通用户验证: + - user `gray20260702112309` + - user id `b4561354-4273-4900-9ada-b6c52d2c5836` + - run id `aa0d1ac1-c03b-4110-baea-6cac83556567` + - request id `gray-code-20260702112309-login` + - status `succeeded` + - attempts `1` +- 事件链: + - `queued` + - `running` + - `tool_gateway_dispatch` + - `tool_gateway_result` + - `succeeded` +- Tool Gateway: + - executor `aider` + - `dryRun=false` + - working dir `/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` + - content `GRAY_CODE_RUN_20260702112309` +- 观察: + - `check-agent-run-worker`: `ok=true`, worker running, queue empty + - SLO: `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no existing production MindSpace deletion or rewrite + - real writes limited to a new test user's MindSpace + +### P6.13 Agent Run Auto-pause Guard + +结果: 通过,自动暂停 guard 已发布到生产 runtime 并安装为 LaunchAgent。 + +- 新增源码: + - `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 copy list updated +- 生产安装: + - label `cn.tkmind.memind-agent-run-guard` + - interval `60s` + - log `/Users/john/Library/Logs/memind-agent-run-guard.log` +- 默认阈值: + - failed runs in 10 minutes `>= 3` + - oldest queued/retryable age `>= 300000ms` + - queued + retryable count `>= 10` + - oldest running age `>= 900000ms` +- 触发动作: + - backup `.env` + - set `MEMIND_AGENT_CODE_RUNS_ENABLED=0` + - set `MEMIND_AGENT_RUN_AUTODISPATCH=1` + - stop and disable `cn.tkmind.memind-agent-run-worker` + - kickstart `cn.tkmind.memind-portal` +- 备份: + - `/Users/john/Project/memind_backups/20260702-113106-agent-run-guard-install` +- 验证: + - `node scripts/agent-run-guard.mjs --dry-run`: `ok=true`, `shouldPause=false` + - guard LaunchAgent enabled, last exit code `0` + - worker remains running + - all-user code-run gate remains enabled + - SLO `ok=true`, `failures=[]` +- Data boundary: + - no schema migration + - no PG data deletion + - no production MindSpace file deletion or rewrite + +### 入口与 Gateway + +结果: 通过。 + +- `https://mm.tkmind.cn/api/status` 返回 `ok`。 +- `H5_PUBLIC_BASE_URL=https://mm.tkmind.cn`。 +- `m.tkmind.cn` 已作为 legacy/rollback 记录,不作为后续 H5 主路径。 +- nginx `mm.tkmind.cn` 已对 SSE 路由禁用 buffering/cache/gzip,并设置长 read/send timeout。 +- SSE route: + - `/api/sessions//events` + - `/api/agent/runs//events` +- `x-accel-buffering: no` 检查通过。 + +评价: + +- Gateway 层已符合 stream-first 的基本要求。 +- 非 SSE 普通 route 仍走统一 `location /`,当前足够。 +- 后续可加 nginx 级别 request id 和 upstream timing log,便于首 token 延迟统计。 + +### Stream Controller + +结果: 通过。 + +- `scripts/check-stream-runtime.mjs` 返回 `ok=true`。 +- SSE HEAD 探测通过,不会打开真实上游 stream。 +- Portal 对 session events 和 agent run events 均具备 no-buffering 响应头。 +- Session SSE 已记录 worker 级 first-token latency EWMA。 +- Session SSE 已记录最近 5 分钟/1 小时 first-token p50/p95。 + +评价: + +- 已完成 P0/P3.5 级别能力。 +- 当前已有 first-token EWMA、p50/p95 窗口趋势和 stream lifecycle counters。 +- 后续建议增加日报落盘或更长周期趋势,不进 PG。 + +### Redis Router / Control Plane + +结果: 通过。 + +- Redis 容器 `memind-runtime-redis` 正常运行。 +- `MEMIND_RUNTIME_REDIS_URL=redis://127.0.0.1:6379/0`。 +- namespace: `memind:runtime`。 +- `/api/runtime/status.router.enabled=true`。 +- worker 状态可见: + - `activeStreams` + - `streamOpenCount` + - `streamAbortCount` + - `streamErrorCount` + - `score` + - `drain` + +评价: + +- Control Plane 已实际承担 runtime state。 +- P5.8 已安装 LaunchAgent,每 60 秒写入 worker metrics,Router 不再只靠请求流量刷新 worker 状态。 +- 当前仍不是 worker self-heartbeat,后续可以改为 sidecar 或 worker 内部心跳。 + +### Goose Worker Pool + +结果: 通过。 + +四个 target 均 healthy: + +- `https://127.0.0.1:18006` +- `https://127.0.0.1:18007` +- `https://127.0.0.1:18008` +- `https://127.0.0.1:18009` + +评价: + +- Worker pool 已可水平调度,并具备 drain 运维入口。 +- 当前调度 scoring 已接入 CPU / memory / fd pressure 的采样指标。 +- first-token latency 已接入 Redis EWMA,并参与 Router score。 +- first-token p50/p95 已按 5 分钟和 1 小时窗口聚合。 + +### Runtime Metrics / SLO + +结果: 通过。 + +- 已安装 LaunchAgent `cn.tkmind.memind-runtime-metrics`。 +- 默认每 60 秒执行 `scripts/runtime-worker-metrics.mjs sample`。 +- 已安装 LaunchAgent `cn.tkmind.memind-runtime-heartbeat`。 +- 默认每 15 秒执行 worker `/status` heartbeat sidecar。 +- `launchctl print` 显示最近运行退出码为 `0`。 +- `scripts/runtime-slo-report.mjs` 返回: + - `ok=true` + - `failures=[]` + - 四个 worker `metricsFresh=true` + - 已展示 `ewmaFirstTokenMs`、`lastFirstTokenMs`、`firstTokenCount` + - 已展示 `firstToken5m`、`firstToken1h` + - 已展示 `toolQueue` + - 已展示 `heartbeatSource=sidecar` + - 已展示 `heartbeatFresh=true` + - 已展示 `heartbeatOk=true` + - `writes.database=false` + - `writes.mindSpace=false` + - `writes.redis=false` +- 生产已观测到真实样本: + - `goosed-4.ewmaFirstTokenMs=125` + - `goosed-4.firstTokenCount=1` + - `goosed-4.firstToken5m.p50Ms=188` + - `goosed-4.firstToken5m.p95Ms=188` + +评价: + +- Observability 从人工检查推进到定时采样 + 一键 SLO 快照。 +- SLO 报告会读取 PG 表行数和 MindSpace 文件统计,但不写入、不删除用户数据。 +- 已有 Redis rolling samples、即时窗口趋势和每日自动 JSON/Markdown SLO 日报。 +- P5.13 已给日报目录增加 30 天保留策略: + - LaunchAgent 参数包含 `--prune --retention-days 30` + - 只清理 `reports/runtime-slo` 顶层过期 `.json/.md` + - 最新报告 `reportPrune.errors=[]` +- P5.14 已给四个 goosed worker 增加常驻 sidecar heartbeat: + - `goosed-1`: `statusCode=200`, `latencyMs=62` + - `goosed-2`: `statusCode=200`, `latencyMs=46` + - `goosed-3`: `statusCode=200`, `latencyMs=46` + - `goosed-4`: `statusCode=200`, `latencyMs=53` + +### Tool Runtime / Aider / OpenHands + +结果: 通过。 + +- `scripts/check-tool-runtime.mjs` 返回 `ok=true`。 +- `role=user` 默认: + - `aider=false` + - `openhands=false` +- 显式白名单用户数: 2。 +- 白名单用户: + - chat mode 不注入 Aider/OpenHands。 + - code mode 可注入 Aider/OpenHands。 +- `toolRuntime.chatInjectsCodeTools=false`。 +- `toolRuntime.codeRunsEnabled=false`。 +- `toolRuntime.queue` 已上线: + - `autoDispatch=true` + - `maxConcurrentRuns=1` + - `runTimeoutMs=900000` + - `inFlight=0` + - `pendingDispatches=0` +- `scripts/agent-run-worker.mjs --status` 已可在生产 runtime 中独立运行。 +- `scripts/agent-run-worker.mjs --once` 已完成真实 chat-mode canary: + - `8e64ff4b-c043-475e-b60b-ae7466111da9` -> `succeeded` + +评价: + +- 已完成高风险工具从普通聊天默认能力中的隔离。 +- 后端 `MEMIND_AGENT_CODE_RUNS_ENABLED` 默认关闭,防止客户端绕过前端灰度。 +- `/agent/runs` 已具备最小 queue gateway: 全局并发限制、运行超时、runtime/SLO 可观测状态。 +- 外部 worker 接管入口已具备,并通过短窗口真实 canary 验证。 +- 生产已恢复 Portal 内部 `autoDispatch=true`。 +- Aider/OpenHands 仍是 session extension 形态,尚未拆成独立工具进程协议。 + +### H5 Code Run 入口 + +结果: 后端 canary 与 H5 UI canary 均已通过,默认关闭。 + +- H5 bundle 已包含 `/agent/runs` 和 `tool_mode` 入口。 +- `VITE_AGENT_CODE_RUNS_ENABLED` 未设置。 +- `VITE_AGENT_CODE_RUNS_AUTODETECT` 未设置。 +- `VITE_AGENT_CODE_RUNS_USER_IDS` 未设置。 +- `MEMIND_AGENT_CODE_RUNS_ENABLED` 未设置。 +- `MEMIND_AGENT_CODE_RUNS_USER_IDS` 未设置。 +- `scripts/check-agent-code-run-entry.mjs` 返回 `ok=true`。 +- P4.10 后端短窗口 code-mode canary 已成功: + - run id: `805e7422-175e-4eb7-8fb2-df0c29f79c0c` + - session id: `20260702_3` + - final status: `succeeded` +- P4.11 已发布默认关闭 runtime: + - release id: `20260702-085001-a0af049` + - `toolRuntime.codeRunsEnabled=false` + - `toolRuntime.chatInjectsCodeTools=false` +- 发布脚本已支持把 `ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1` 传到远端 post-check。 +- 已按用户要求忽略既有生产 MindSpace 公开页缺失 docx 附件链接,不修复。 +- P4.12 H5 页面编辑 UI canary 已完成: + - canary release: `20260702-085319-a0af049` + - canary run id: `825720c9-330d-447d-a6ed-afeabbf6e14c` + - canary session id: `20260702_4` + - final status: `succeeded` + - attempts: `1` + - restored release: `20260702-085706-a0af049` + - restored `toolRuntime.codeRunsEnabled=false` + - restored `.env` 无 `MEMIND_AGENT_CODE_RUNS_*` +- 脚本明确: + - `writes.database=false` + - `writes.mindSpace=false` + - `createsAgentRun=false` + +评价: + +- 灰度链路已经有前后端双门禁,并补齐用户级白名单。 +- 当前生产不会自动启用 code run。 +- 当前已恢复默认关闭;后续如要放量,可只对白名单重新打开,不开普通聊天 autodetect。 + +### Persistence / PG / MindSpace + +结果: 通过。 + +- 本轮 P4.6-P6.2/P4.10 未做 schema migration。 +- 本轮未删除 PG 数据。 +- 本轮未删除 MindSpace 数据。 +- P5.8 SLO 报告只读统计 PG 和 MindSpace。 +- P5.13 SLO 报告保留策略只清理运维报告目录。 +- P6.2/P4.10 仅新增 canary run/session/event 审计记录。 +- 当前 `MindSpace`: + - `/Users/john/Project/Memind/MindSpace`: `files=2143`, `dirs=1595`, `bytes=122160654` + - `/Users/john/Project/Memind/data/mindspace`: `files=5281`, `dirs=7651`, `bytes=270214686` +- 已有生产备份: + - `20260702-065813-pre-p35-p55` + - `20260702-071604-pre-p46-code-task-entry` + - `20260702-071914-pre-p47-h5-code-run-entry` + - `20260702-072233-pre-p48-code-run-check` +- `20260702-072452-pre-p49-code-run-server-gate` + - `20260702-073927-pre-p58-runtime-slo-agent` + - `20260702-074955-pre-p60-tool-queue-v0` + - `20260702-075904-pre-p61-agent-run-worker-v1` + - `20260702-081928-pre-p62-tool-worker-canary` + - `20260702-083225-pre-p410-code-run-canary` + - `20260702-083917-pre-p513-slo-prune` + - `memind-full-20260702-084418-c7d8140-before.tar.gz` + - `memind-persisted-20260702-084418-c7d8140-before.tar.gz` + - `memind-full-20260702-085001-a0af049-before.tar.gz` + - `memind-persisted-20260702-085001-a0af049-before.tar.gz` + - `20260702-085302-pre-p412-h5-ui-canary` + - `memind-full-20260702-085319-a0af049-before.tar.gz` + - `memind-persisted-20260702-085319-a0af049-before.tar.gz` + - `memind-full-20260702-085706-a0af049-before.tar.gz` + - `memind-persisted-20260702-085706-a0af049-before.tar.gz` + - `memind-full-20260702-090343-e24e6ca-before.tar.gz` + - `memind-persisted-20260702-090343-e24e6ca-before.tar.gz` + +评价: + +- 数据保护执行良好。 +- 建议每次进入“会创建真实 agent run 的 canary”前,再做一次轻量 DB/MindSpace 快照或至少记录 row/file count baseline。 + +### Source Sync / Remote Dev + +结果: 通过。 + +- 干净分支当前 clean。 +- 分支已推送: + - `memind-streaming-runtime-20260702` +- 远程开发机同步命令: + - `git pull origin memind-streaming-runtime-20260702` + +评价: + +- 已满足“远程开发电脑保持一致、后续直接拉分支”的要求。 +- 生产 runtime 是单文件 artifact,源码分支保留 `.runtime/portal/server.mjs` 便于复现生产部署。 + +## 当前架构完成度 + +| 模块 | 完成度 | 状态 | +| --- | ---: | --- | +| Edge Gateway / mm domain | 85% | 已切主路径,SSE 配置正确 | +| Stream Controller | 86% | pipeline 已落地,first-token EWMA 和 p50/p95 已接入 | +| Redis Control Plane | 82% | runtime state 已迁移,worker sidecar heartbeat 已上线 | +| Worker Pool | 86% | 四 worker healthy,drain 可用,CPU/memory/fd 指标和主动 heartbeat 已接入 | +| Tool Isolation | 99% | chat/code 隔离完成,queue gateway v0、外部 worker 入口、真实 canary、`agent-run-v1` 协议、Aider/OpenHands 真实 canary、产物校验、worker LaunchAgent、validated worker canary、worker check、rollout gates、全用户灰度和自动暂停 guard 已完成 | +| H5 Code Run Canary | 99% | 后端 code-mode canary、用户级 gate、H5 页面编辑 UI canary、external worker 精确接管 canary、guardrails、validated worker canary、rollout policy、全用户真实路径验证已完成,当前生产处于 all-user gray | +| Persistence Boundary | 85% | PG/MindSpace 保护良好,SLO 脚本只读验证 | +| Observability | 96% | 定时 metrics、sidecar heartbeat、SLO 快照/日报/30天保留、first-token EWMA 与 p50/p95 已落地 | +| Remote Sync | 90% | 干净分支已稳定推送 | + +## 风险清单 + +1. activeStreams 可能残留。 + - 影响: score 会偏高,长期可能影响调度。 + - 状态: P5.15 Active Stream TTL Reconcile 已按用户要求跳过,暂不做报表/定时 reconcile。 + +2. Tool Gateway 已进入全用户长期灰度,需持续观察。 + - 影响: code-run 现在会对所有 H5 用户自动启用;external worker 单并发处理,guard 会在失败或积压超过阈值时自动暂停。 + - 状态: P6.12/P6.13 已完成;当前 `codeRunsEnabled=true`、`autoDispatch=false`、external worker running、guard enabled。 + - 建议: 保持 30-60 分钟密集观察,然后转为日报/SLO + guard 常态观察。 + +3. 生产 MindSpace 存在既有公开页缺失 docx 附件链接。 + - 影响: 已按用户要求作为历史内容问题忽略,不阻断 streaming runtime 发布。 + - 建议: 不在本轮修复;后续只有用户重新要求时再单独处理。 + +## 下一步执行建议 + +### P6.14 Gray Ops Observation + +- 观察 30-60 分钟: + - `MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=1 node scripts/check-agent-run-worker.mjs` + - `node scripts/agent-run-guard.mjs --dry-run` + - `node scripts/runtime-slo-report.mjs` + - `tail -200 ~/Library/Logs/memind-agent-run-worker.log` + - `tail -200 ~/Library/Logs/memind-agent-run-guard.log` +- 重点判断: + - queue 是否持续为空或快速归零。 + - 是否出现新的 failed run。 + - guard 是否误触发或正确保持 `shouldPause=false`。 + - SLO 是否仍 `failures=[]`。 +- 观察稳定后再评估: + - 是否保持 worker 单并发。 + - 是否把 validation metadata 自动补入 H5 默认 code-run。 + - 是否增加任务级产物规范和用户可见进度/失败说明。 diff --git a/docs/architecture/memind-2-streaming-agent-runtime-plan.md b/docs/architecture/memind-2-streaming-agent-runtime-plan.md index 5c54633..e36e3a7 100644 --- a/docs/architecture/memind-2-streaming-agent-runtime-plan.md +++ b/docs/architecture/memind-2-streaming-agent-runtime-plan.md @@ -9,9 +9,28 @@ - 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。 +- 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 生产化补强: 进行中,先完成生产数据库和 `MindSpace` 备份,再增加 runtime metrics、健康检查脚本和 drain 运维脚本。 +- P3.5/P5.5 生产化补强: 已完成 runtime metrics、健康检查脚本、drain 运维脚本、自动采样 LaunchAgent 和 SLO 只读报告。 ## 目标 @@ -501,6 +520,2065 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr - 本阶段不写数据库、不删除数据、不修改 `/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=false` + - `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` + ## 回滚策略 - P0: 修改前保留 `server.mjs` 备份;如启动失败,恢复备份并 `launchctl kickstart` Portal。 diff --git a/scripts/agent-run-guard.mjs b/scripts/agent-run-guard.mjs new file mode 100755 index 0000000..e6a568a --- /dev/null +++ b/scripts/agent-run-guard.mjs @@ -0,0 +1,305 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import mysql from 'mysql2/promise'; + +const execFileAsync = promisify(execFile); +const DEFAULT_WORKER_LABEL = 'cn.tkmind.memind-agent-run-worker'; +const DEFAULT_PORTAL_LABEL = 'cn.tkmind.memind-portal'; + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx < 0) continue; + const key = trimmed.slice(0, idx).trim(); + let value = trimmed.slice(idx + 1).trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + if (!process.env[key]) process.env[key] = value; + } +} + +function truthy(value) { + return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase()); +} + +function positiveInt(value, fallback) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; +} + +function parseArgs(argv) { + const args = { + apply: argv.includes('--apply'), + dryRun: argv.includes('--dry-run') || !argv.includes('--apply'), + help: argv.includes('--help') || argv.includes('-h'), + }; + return args; +} + +function printHelp() { + console.log([ + 'Usage:', + ' node scripts/agent-run-guard.mjs [--dry-run]', + ' node scripts/agent-run-guard.mjs --apply', + '', + 'Dry-run is the default. --apply can stop the external worker and disable code-run gate in .env.', + ].join('\n')); +} + +function parseMysqlConfig() { + if (process.env.DATABASE_URL) { + const url = new URL(process.env.DATABASE_URL); + if (url.protocol !== 'mysql:') { + throw new Error(`Unsupported DATABASE_URL scheme for agent run guard: ${url.protocol}`); + } + return { + host: url.hostname, + port: Number(url.port || 3306), + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: url.pathname.replace(/^\/+/, ''), + charset: 'utf8mb4', + }; + } + return { + host: process.env.MYSQL_HOST, + port: Number(process.env.MYSQL_PORT || 3306), + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + charset: 'utf8mb4', + }; +} + +async function runCommand(command, args) { + try { + const { stdout, stderr } = await execFileAsync(command, args, { maxBuffer: 1024 * 1024 }); + return { ok: true, stdout, stderr }; + } catch (err) { + return { + ok: false, + code: err?.code ?? null, + stdout: err?.stdout ?? '', + stderr: err?.stderr ?? '', + message: err instanceof Error ? err.message : String(err), + }; + } +} + +function replaceOrAppendEnv(raw, updates) { + const pending = new Map(Object.entries(updates)); + const lines = raw.split('\n'); + const next = lines.map((line) => { + const match = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*=).*/); + if (!match) return line; + const key = match[2]; + if (!pending.has(key)) return line; + const value = pending.get(key); + pending.delete(key); + return `${key}=${value}`; + }); + if (pending.size > 0) { + if (next.length > 0 && next[next.length - 1] !== '') next.push(''); + next.push('## agent-run guard auto-disable'); + for (const [key, value] of pending) next.push(`${key}=${value}`); + } + return next.join('\n').replace(/\n*$/, '\n'); +} + +function timestamp() { + const d = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; +} + +async function readQueueHealth(now) { + const conn = await mysql.createConnection(parseMysqlConfig()); + try { + const [statusRows] = await conn.query( + `SELECT status, COUNT(*) AS count + FROM h5_agent_runs + WHERE status IN ('queued', 'running', 'retryable') + GROUP BY status`, + ); + const statusCounts = {}; + for (const row of statusRows) statusCounts[row.status] = Number(row.count ?? 0); + + const [oldestPendingRows] = await conn.query( + `SELECT MIN(updated_at) AS oldest_updated_at + FROM h5_agent_runs + WHERE status IN ('queued', 'retryable')`, + ); + const oldestPendingUpdatedAt = oldestPendingRows[0]?.oldest_updated_at == null + ? null + : Number(oldestPendingRows[0].oldest_updated_at); + + const [oldestRunningRows] = await conn.query( + `SELECT MIN(started_at) AS oldest_started_at + FROM h5_agent_runs + WHERE status = 'running'`, + ); + const oldestRunningStartedAt = oldestRunningRows[0]?.oldest_started_at == null + ? null + : Number(oldestRunningRows[0].oldest_started_at); + + const failedWindowMs = positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_FAILED_WINDOW_MS, 10 * 60 * 1000); + const since = now - failedWindowMs; + const [failedRows] = await conn.query( + `SELECT COUNT(*) AS count + FROM h5_agent_runs + WHERE status = 'failed' AND updated_at >= ?`, + [since], + ); + const [latestFailedRows] = await conn.query( + `SELECT id, request_id, error_message, updated_at + FROM h5_agent_runs + WHERE status = 'failed' + ORDER BY updated_at DESC + LIMIT 1`, + ); + + return { + statusCounts, + queuedOrRetryable: Number(statusCounts.queued ?? 0) + Number(statusCounts.retryable ?? 0), + running: Number(statusCounts.running ?? 0), + oldestPendingUpdatedAt, + oldestPendingAgeMs: oldestPendingUpdatedAt == null ? 0 : Math.max(0, now - oldestPendingUpdatedAt), + oldestRunningStartedAt, + oldestRunningAgeMs: oldestRunningStartedAt == null ? 0 : Math.max(0, now - oldestRunningStartedAt), + failedWindowMs, + failedRecentCount: Number(failedRows[0]?.count ?? 0), + latestFailedRun: latestFailedRows[0] ? { + id: latestFailedRows[0].id, + requestId: latestFailedRows[0].request_id, + error: latestFailedRows[0].error_message, + updatedAt: Number(latestFailedRows[0].updated_at ?? 0), + } : null, + }; + } finally { + await conn.end(); + } +} + +function evaluateHealth(queue) { + const thresholds = { + maxRecentFailures: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_RECENT_FAILURES, 3), + maxPendingAgeMs: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_PENDING_AGE_MS, 5 * 60 * 1000), + maxPendingCount: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_PENDING_COUNT, 10), + maxRunningAgeMs: positiveInt(process.env.MEMIND_AGENT_RUN_GUARD_MAX_RUNNING_AGE_MS, 15 * 60 * 1000), + }; + const reasons = []; + if (queue.failedRecentCount >= thresholds.maxRecentFailures) { + reasons.push(`recent_failed_count ${queue.failedRecentCount} >= ${thresholds.maxRecentFailures}`); + } + if (queue.oldestPendingAgeMs >= thresholds.maxPendingAgeMs) { + reasons.push(`oldest_pending_age_ms ${queue.oldestPendingAgeMs} >= ${thresholds.maxPendingAgeMs}`); + } + if (queue.queuedOrRetryable >= thresholds.maxPendingCount) { + reasons.push(`pending_count ${queue.queuedOrRetryable} >= ${thresholds.maxPendingCount}`); + } + if (queue.oldestRunningAgeMs >= thresholds.maxRunningAgeMs) { + reasons.push(`oldest_running_age_ms ${queue.oldestRunningAgeMs} >= ${thresholds.maxRunningAgeMs}`); + } + return { thresholds, reasons, shouldPause: reasons.length > 0 }; +} + +async function disableCodeRuns({ root, envFile, workerLabel, portalLabel, gui, reasons, dryRun }) { + const actions = []; + const updates = { + MEMIND_AGENT_CODE_RUNS_ENABLED: '0', + MEMIND_AGENT_RUN_AUTODISPATCH: '1', + }; + if (dryRun) { + return { + applied: false, + actions: [ + `would backup ${envFile}`, + `would set ${Object.entries(updates).map(([k, v]) => `${k}=${v}`).join(', ')}`, + `would stop and disable ${workerLabel}`, + `would kickstart ${portalLabel}`, + ], + }; + } + + const backupRoot = process.env.MEMIND_AGENT_RUN_GUARD_BACKUP_DIR + || path.join(path.dirname(root), 'memind_backups', `${timestamp()}-agent-run-guard-pause`); + fs.mkdirSync(backupRoot, { recursive: true }); + const envBackup = path.join(backupRoot, '.env.before'); + fs.copyFileSync(envFile, envBackup); + actions.push(`backed_up_env:${envBackup}`); + + const raw = fs.readFileSync(envFile, 'utf8'); + const updated = replaceOrAppendEnv(raw, updates); + const marker = [ + '', + `# agent-run guard pause at ${new Date().toISOString()}`, + `# reasons: ${reasons.join('; ')}`, + ].join('\n'); + fs.writeFileSync(envFile, `${updated.replace(/\n*$/, '\n')}${marker}\n`, 'utf8'); + actions.push(`updated_env:${envFile}`); + + await runCommand('launchctl', ['bootout', `${gui}/${workerLabel}`]); + await runCommand('launchctl', ['disable', `${gui}/${workerLabel}`]); + actions.push(`disabled_worker:${workerLabel}`); + + const kick = await runCommand('launchctl', ['kickstart', '-k', `${gui}/${portalLabel}`]); + actions.push(kick.ok ? `kickstarted_portal:${portalLabel}` : `portal_kickstart_failed:${kick.message}`); + return { applied: true, backupRoot, actions }; +} + +const args = parseArgs(process.argv.slice(2)); +if (args.help) { + printHelp(); + process.exit(0); +} + +const root = path.join(path.dirname(new URL(import.meta.url).pathname), '..'); +const envFile = process.env.MEMIND_ENV_FILE || path.join(root, '.env'); +loadEnvFile(envFile); + +const enabled = process.env.MEMIND_AGENT_RUN_GUARD_ENABLED == null + ? true + : truthy(process.env.MEMIND_AGENT_RUN_GUARD_ENABLED); +const workerLabel = process.env.MEMIND_AGENT_RUN_WORKER_LABEL || DEFAULT_WORKER_LABEL; +const portalLabel = process.env.MEMIND_PORTAL_LABEL || DEFAULT_PORTAL_LABEL; +const gui = `gui/${process.getuid()}`; +const now = Date.now(); +const queue = await readQueueHealth(now).catch((err) => ({ + error: err instanceof Error ? (err.message || err.code || err.name) : String(err), +})); +const evaluation = queue.error + ? { thresholds: {}, reasons: [`queue_health_error ${queue.error}`], shouldPause: false } + : evaluateHealth(queue); +let pause = { applied: false, actions: [] }; + +if (enabled && evaluation.shouldPause) { + pause = await disableCodeRuns({ + root, + envFile, + workerLabel, + portalLabel, + gui, + reasons: evaluation.reasons, + dryRun: args.dryRun, + }); +} + +const result = { + ok: enabled ? !evaluation.shouldPause || pause.applied || args.dryRun : true, + checkedAt: new Date(now).toISOString(), + enabled, + mode: args.apply ? 'apply' : 'dry-run', + queue, + evaluation, + pause, +}; + +console.log(JSON.stringify(result, null, 2)); +process.exit(result.ok ? 0 : 1); diff --git a/scripts/build-portal-runtime.mjs b/scripts/build-portal-runtime.mjs index 7e71b8b..13ef6f9 100755 --- a/scripts/build-portal-runtime.mjs +++ b/scripts/build-portal-runtime.mjs @@ -335,6 +335,14 @@ async function writeMetadata() { path.join(root, 'scripts', 'check-agent-run-worker.mjs'), path.join(runtimeRoot, 'scripts', 'check-agent-run-worker.mjs'), ); + await fs.copyFile( + path.join(root, 'scripts', 'agent-run-guard.mjs'), + path.join(runtimeRoot, 'scripts', 'agent-run-guard.mjs'), + ); + await fs.copyFile( + path.join(root, 'scripts', 'install-agent-run-guard-agent.sh'), + path.join(runtimeRoot, 'scripts', 'install-agent-run-guard-agent.sh'), + ); await writeFile( path.join(runtimeRoot, 'RUNBOOK.txt'), [ @@ -397,6 +405,9 @@ async function writeMetadata() { ' node scripts/agent-run-worker.mjs --once', ' bash scripts/install-agent-run-worker-agent.sh # installs disabled by default', ' node scripts/check-agent-run-worker.mjs # read-only LaunchAgent/queue check', + ' node scripts/agent-run-guard.mjs # dry-run auto-pause guard check', + ' node scripts/agent-run-guard.mjs --apply # stop worker and disable code-run gate when thresholds trip', + ' bash scripts/install-agent-run-guard-agent.sh # installs guard LaunchAgent', '', ].join('\n'), ); @@ -425,6 +436,8 @@ async function main() { await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-runtime-heartbeat-agent.sh'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-runtime-slo-report-agent.sh'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-agent-run-worker-agent.sh'), 0o755); + await fs.chmod(path.join(runtimeRoot, 'scripts', 'agent-run-guard.mjs'), 0o755); + await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-agent-run-guard-agent.sh'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-agent-run-worker.mjs'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'memind-portal-tunnel.sh'), 0o755); diff --git a/scripts/install-agent-run-guard-agent.sh b/scripts/install-agent-run-guard-agent.sh new file mode 100755 index 0000000..9f87abd --- /dev/null +++ b/scripts/install-agent-run-guard-agent.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +SCRIPT="${MEMIND_AGENT_RUN_GUARD_SCRIPT:-$ROOT/scripts/agent-run-guard.mjs}" +LABEL="${MEMIND_AGENT_RUN_GUARD_LABEL:-cn.tkmind.memind-agent-run-guard}" +PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG="${MEMIND_AGENT_RUN_GUARD_LOG:-$HOME/Library/Logs/memind-agent-run-guard.log}" +GUI="gui/$(id -u)" +INTERVAL="${MEMIND_AGENT_RUN_GUARD_INTERVAL_SECONDS:-60}" +START="${MEMIND_AGENT_RUN_GUARD_START:-1}" + +mkdir -p "$HOME/Library/LaunchAgents" "$(dirname "$LOG")" + +if [[ ! -x "$NODE_BIN" ]]; then + NODE_BIN="$(command -v node)" +fi +if [[ ! -f "$SCRIPT" ]]; then + echo "agent run guard script not found: $SCRIPT" >&2 + exit 1 +fi + +cat > "$PLIST" < + + + + Label + $LABEL + ProgramArguments + + $NODE_BIN + $SCRIPT + --apply + + WorkingDirectory + $ROOT + StartInterval + $INTERVAL + RunAtLoad + + StandardOutPath + $LOG + StandardErrorPath + $LOG + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin + MEMIND_AGENT_RUN_GUARD_ENABLED + ${MEMIND_AGENT_RUN_GUARD_ENABLED:-1} + MEMIND_AGENT_RUN_GUARD_FAILED_WINDOW_MS + ${MEMIND_AGENT_RUN_GUARD_FAILED_WINDOW_MS:-600000} + MEMIND_AGENT_RUN_GUARD_MAX_RECENT_FAILURES + ${MEMIND_AGENT_RUN_GUARD_MAX_RECENT_FAILURES:-3} + MEMIND_AGENT_RUN_GUARD_MAX_PENDING_AGE_MS + ${MEMIND_AGENT_RUN_GUARD_MAX_PENDING_AGE_MS:-300000} + MEMIND_AGENT_RUN_GUARD_MAX_PENDING_COUNT + ${MEMIND_AGENT_RUN_GUARD_MAX_PENDING_COUNT:-10} + MEMIND_AGENT_RUN_GUARD_MAX_RUNNING_AGE_MS + ${MEMIND_AGENT_RUN_GUARD_MAX_RUNNING_AGE_MS:-900000} + + + +EOF + +plutil -lint "$PLIST" +launchctl bootout "$GUI/$LABEL" 2>/dev/null || true +launchctl bootstrap "$GUI" "$PLIST" + +if [[ "$START" == "1" || "$START" == "true" || "$START" == "yes" ]]; then + launchctl enable "$GUI/$LABEL" + launchctl kickstart -k "$GUI/$LABEL" + state="started" +else + launchctl disable "$GUI/$LABEL" 2>/dev/null || true + state="installed-disabled" +fi + +echo "installed $PLIST" +echo "state: $state" +echo "script: $SCRIPT" +echo "interval_seconds: $INTERVAL" +echo "log: $LOG" +echo "manual stop: launchctl bootout $GUI/$LABEL"