Parcourir la source

test: 30 路并发压测 + 交付链路根因定位

压测脚本 scripts/loadtest-agent.mjs:
- 用 RP5Ht55NeX 为测试账号签身份,外部开 N 个独立房间并发投递
- 三重验证:/deliverables/ 前缀 + S3 实际 GET + 文件内容含本房 token
- 强制走 MEDIA: 行,避免容器用 uploader.mjs 旁路直传导致假测试

压测结果:
- 10 路:10/10,0 串台,p50 9.3s
- 30 路:30/30,0 串台,p50 127.1s
- 30 路资源:mem +7MB、load 2.66/64 核、无 OOM/429
- 文件层面完美:30 个文件各 120 行约 8.8KB,串台 0

🔴 交付链路 100% 失败(压测前就坏了):根因是容器走
AlP56LCKFm 旁路直传,链接前缀 /user/<uid>/report/ 而非
/deliverables/<agentId>/,App 不认;且未写台账。

另记录:enqueueArtifact/pendingArtifacts 属于 lfYlgU7SkK
而非 THXAOGKiPK;自定义云函数必须走 /api/functions 包装层。

Co-Authored-By: Claude Code <noreply@anthropic.com>
Huccct il y a 14 heures
Parent
commit
5a804c1513

+ 21 - 0
docs/00-tracking/操作追踪.md

@@ -8178,3 +8178,24 @@ URL 不会重复显示:`chat.service.ts:2349` 的 `stripLinks` 会把已提取
 | **交付文档** | 新建 `docs/03-interface/容器接入App-完整操作手册-20261114.md`:身份/Matrix 契约/插件清单/文件链路/状态上报/finish 守卫/GC/调度器/12 条踩坑/五阶段检查表/两台对照 |
 | **验证** | 手册内所有事实均核对过源码(`parse-api.ts:68`、`matrix-client.service.ts:318/1413/1455`、`s3-link.ts:290`、`jobs.py:772`、`scheduler_script.py:257`),无推测内容 |
 | **未验证** | 「容器**重建**后守卫与调度仍在」—— 两台容器自 11-09 起从未重启过,只有静态依据(东西都在 `/opt/data` 持久卷 + `config.yaml` enabled),无实测 |
+
+## 2026-11-14(续五)· 30 路并发压测 + 交付链路根因定位
+
+| 项 | 内容 |
+|---|---|
+| **压测方式** | 新增 `scripts/loadtest-agent.mjs`:用云函数 `RP5Ht55NeX` 为测试账号(`ksftest01`/`yObhYHzjCx`)签身份,外部开 N 个独立房间并发发指令 |
+| **为什么需要它** | 容器自己造不出 N 个独立会话(它只有 agent 身份,无用户侧身份,4 条路全不通) |
+| **阶段 0(1 路)** | ✅ 链路通,4 秒上传,s3 可访问。⚠️ 但容器走 `uploader.mjs` 直传,`pushed` 零记录 |
+| **阶段 1(10 路)** | ✅ 10/10 完成,0 串台,p50 9.3s(与 2 路的 9.2s 几乎相同 → 无排队) |
+| **阶段 2(30 路)** | ✅ **30/30 完成,0 串台**,p50 **127.1s**,max 145.2s |
+| **30 路资源** | mem 峰值 16475MB(基线 16442,**+7MB**)、load 2.66/64 核、kernels 20、**无 OOM/崩溃/429** |
+| **耗时跳变分析** | 前 110 秒 0 完成 → 112s 完成 2 → 128s 完成 17 → 143s 完成 28。**排队后批量释放**,非资源瓶颈(内存/CPU 都很低),疑似上游模型 API 并发限制 |
+| **文件层面(压测核心)** | 30 个文件全到位,每个 120 行约 8.7~8.9KB,**全量交叉检测串台 = 0** |
+| **🔴 交付链路 100% 失败** | 30/30 回复都是 `⚠️ Couldn't deliver the file attachment` |
+| **根因(由我独立定位)** | `enqueueArtifact`/`pendingArtifacts` 属于 **`lfYlgU7SkK`**(fmodeagent-deliverables v0.1.0,注释原文确认),**不属于 `THXAOGKiPK`**。实测 `THXAOGKiPK` 只有 6 个 action:`listBySession/registerFile/listByUrl/listByAgent/listByGroup/claimToGroup` |
+| **更深层根因** | 容器走 `fmode_outbound_media.upload_bytes()` **旁路直传**(`AlP56LCKFm` 路径),链接前缀为 `/user/<uid>/report/...`,非 `/deliverables/<agentId>/...`。App `s3-link.ts:290` 不认 |
+| **lj-test 给出的定论** | 「`AlP56LCKFm` 的 `user/` 前缀是**服务端强制**,客户端剥不掉。要拿 `deliverables/` 前缀,**必须换函数**(走 `lfYlgU7SkK` 队列),不是换参数」 |
+| **平台调用方式(新发现)** | 自定义云函数**不能用标准 Parse REST**:`POST /parse/functions/<id>` + masterKey → `{"code":141,"error":"Invalid function"}`。必须走 `/api/functions` 的 `{id, token, params}` 包装层 |
+| **用户澄清** | 文件**不走 Matrix 传输**;容器上传完成后返回 URL 给 App,同时写台账入库(对应用户数据)。参考 lj-test 逻辑 |
+| **下一步** | 向 lj-test 取「通知平台→领任务→上传→登记→发 m.file」五步真实代码,让 kangshifu 逐行照抄 |
+| **验证** | `scripts/loadtest-agent.mjs` 语法校验通过;三轮压测报告落盘 `loadtest-smoke-n2.md` / `loadtest-n10.md` / `loadtest-n30.md` |

+ 554 - 0
docs/03-interface/kangshifu-30路核对-回答.md

@@ -0,0 +1,554 @@
+# 容器取证问答:agent-node-kangshifu
+
+- 时间:2026-11-14T03:43:12.806Z
+- 房间:`!jp1BsE_PIDfu7JjZ83E_515i2gbjgb9eWzykAclqqdk`(临时房间,可保留查阅)
+- 消息统计:正文 30 段 / 工具过程 1 段 / 文件 0 个
+- 结束原因:收到 ✅ reaction
+
+## 我发出的问题
+
+```text
+# 授权:采样日志可以写 /tmp
+
+## 回答你的问题
+
+**是的,授权**。采样日志可以写 `/tmp`。这条不算「主动写文件」的禁令范围 —— 禁令针对的是**不要主动产出交付物**,采样器日志是观测工具本身。
+
+```bash
+nohup sh -c 'for i in $(seq 1 90); do
+  M=$(cat /sys/fs/cgroup/memory.current)
+  P=$(ps aux | wc -l)
+  L=$(cut -d" " -f1 /proc/loadavg)
+  R=$(ls -d /tmp/hermes_kernel_* 2>/dev/null | wc -l)
+  echo "$(date +%H:%M:%S) mem_mb=$((M/1048576)) procs=$P load=$L kernels=$R"
+  sleep 10
+done' > /tmp/loadtest-sample.log 2>&1 &
+echo "采样已启动 PID=$!"
+```
+
+**现在就启动,然后不用管它。**
+
+---
+
+## 30 路压测马上开始(我从外部发)
+
+### 这次是真测试,指令会强制走交付链路
+
+每条消息要求:
+1. 用 shell 创建 `/opt/data/work/loadtest-n30/msg-<i>.md`
+2. 内容约 **8KB**(120 行,每行带唯一 token)
+3. 回复正文里**单独一行**写 `MEDIA:/opt/data/work/loadtest-n30/msg-<i>.md`
+4. 回复里原样出现 token
+
+**第 3 条是关键** —— 让它走 `plugin-deliverable-inject` → 出站媒体 → `/deliverables/` 前缀。
+
+🔴 **如果这条链路是坏的(比如 `plugin-deliverable-inject` 没装或没生效),请如实告诉我,不要自己改走 `uploader.mjs` 绕过。**
+绕过的话链接会是 `/user/<uid>/report/...`,App 不认,测出来是假的。
+
+---
+
+## 压测结束后核对(等我说「压测结束」)
+
+```bash
+echo "=== ① 文件数量(应 30 个)==="
+ls -la /opt/data/work/loadtest-n30/ 2>&1 | head -40
+ls /opt/data/work/loadtest-n30/*.md 2>/dev/null | wc -l
+
+echo "=== ② 抽样看 3 个文件内容(验证没错乱/没串台)==="
+for i in 1 15 30; do
+  f=/opt/data/work/loadtest-n30/msg-$i.md
+  echo "--- msg-$i.md"
+  head -2 "$f" 2>&1
+  echo "   行数=$(wc -l < "$f" 2>/dev/null)  字节=$(stat -c%s "$f" 2>/dev/null)"
+done
+
+echo "=== ③ 文件里有没有别的房的 token(串台检测)==="
+python3 - <<'PY'
+import glob, re, os
+files = sorted(glob.glob('/opt/data/work/loadtest-n30/msg-*.md'))
+tokens = {}
+for f in files:
+    m = re.search(r'token=(T\d+-[A-Z0-9]+)', open(f, errors='ignore').read())
+    if m: tokens[f] = m.group(1)
+allt = set(tokens.values())
+for f, t in tokens.items():
+    txt = open(f, errors='ignore').read()
+    leak = [x for x in allt if x != t and x in txt]
+    if leak:
+        print(f"🔴 串台 {os.path.basename(f)}: {leak}")
+print(f"共 {len(files)} 文件,{len(allt)} 个不同 token,串台 {sum(1 for f,t in tokens.items() if any(x!=t and x in open(f,errors='ignore').read() for x in allt))} 个")
+PY
+
+echo "=== ④ pushed 登记(注意:这条链路可能本来就没在跑)==="
+python3 - <<'PY'
+import json, glob
+hits = 0
+for f in glob.glob('/opt/data/cache/deliverable-hook/*.json'):
+    d = json.load(open(f))
+    for p in (d.get('pushed') or []):
+        if 'loadtest-n30' in json.dumps(p): hits += 1
+print("pushed 里 loadtest-n30 条数 =", hits)
+PY
+
+echo "=== ⑤ 采样日志 ==="
+cat /tmp/loadtest-sample.log
+```
+
+---
+
+## 回给我的格式
+
+```
+## 采样
+<全部行>
+
+## 峰值 / 前后
+mem 峰值 = ? MB @ ?
+procs 峰值 = ?
+load 峰值 = ?
+kernels 峰值 = ?
+开始前 = ? MB   结束后 = ? MB   差值 = ?
+
+## 文件
+文件数 = ? / 30
+抽样内容 = ?
+串台 = ? 个
+
+## 交付链路
+回复里出现了 /deliverables/ 链接吗?出现几个?
+(没有的话:是 plugin-deliverable-inject 的问题,还是你没按要求写 MEDIA 行?如实说)
+
+## 异常
+OOM / 崩溃 / 429 / 402 / timeout?
+```
+
+🔴 **不要绕过链路。** 链路坏了就报坏,我要知道真实状态。
+```
+
+## 容器回答(原文,按时间顺序,未删改)
+
+### 1【prose】
+
+```text
+我先启动采样,然后立刻核实交付链路是否真的在跑。
+```
+
+### 2【prose】
+
+```text
+采样进程被系统拦了,得用 Hermes 管的方式起。同时插件目录是在的,我继续查链路。
+```
+
+### 3【prose】
+
+```text
+```
+for i in $(seq 1 90); do M=$(cat /sys...
+```
+```
+
+### 4【prose】
+
+```text
+采样已在跑(PID 137642)。压测消息已经在陆续进来——先看链路和已有文件的真实状态。
+```
+
+### 5【prose】
+
+```text
+```
+echo "=== 采样确认 ==="; wc -l /tmp/loadt...
+```
+```
+
+### 6【prose】
+
+```text
+```
+echo "=== 插件 hook 里的 URL 前缀逻辑 ==="; g...
+```
+```
+sed -n '1,120p' /opt/data/plugins/plu...
+```
+```
+
+### 7【prose】
+
+```text
+* ```
+echo "=== 插件 hook 里的 URL 前缀逻辑 ==="; g...
+```
+```
+sed -n '1,120p' /opt/data/plugins/plu...
+```
+```
+echo "=== 是否存在 fmode-deliverable-hook...
+```
+```
+
+### 8【prose】
+
+```text
+* ```
+echo "=== 插件 hook 里的 URL 前缀逻辑 ==="; g...
+```
+```
+sed -n '1,120p' /opt/data/plugins/plu...
+```
+```
+echo "=== 是否存在 fmode-deliverable-hook...
+```
+```
+sed -n '120,260p' /opt/data/plugins/p...
+```
+```
+sed -n '260,400p' /opt/data/plugins/p...
+```
+```
+
+### 9【prose】
+
+```text
+* ```
+echo "=== 插件 hook 里的 URL 前缀逻辑 ==="; g...
+```
+```
+sed -n '1,120p' /opt/data/plugins/plu...
+```
+```
+echo "=== 是否存在 fmode-deliverable-hook...
+```
+```
+sed -n '120,260p' /opt/data/plugins/p...
+```
+```
+sed -n '260,400p' /opt/data/plugins/p...
+```
+```
+echo "=== fmode_outbound_media.py 里 u...
+```
+```
+sed -n '181,240p' /opt/data/scripts/f...
+```
+```
+
+### 10【prose】
+
+```text
+采样进程似乎挂了,日志只有 1 行且文件数停在 8。我先确认真实状态:
+```
+
+### 11【prose】
+
+```text
+* ```
+echo "=== 插件 hook 里的 URL 前缀逻辑 ==="; g...
+```
+```
+sed -n '1,120p' /opt/data/plugins/plu...
+```
+```
+echo "=== 是否存在 fmode-deliverable-hook...
+```
+```
+sed -n '120,260p' /opt/data/plugins/p...
+```
+```
+sed -n '260,400p' /opt/data/plugins/p...
+```
+```
+echo "=== fmode_outbound_media.py 里 u...
+```
+```
+sed -n '181,240p' /opt/data/scripts/f...
+```
+```
+echo "=== 采样 ==="; cat /tmp/loadtest-...
+```
+```
+date '+%H:%M:%S'; echo "--- 采样进程 ---"...
+```
+```
+
+### 12【prose】
+
+```text
+采样正常运行:11:40:58 第 2 行已出。刚才只是缓冲延迟。现在 8 个文件不再增长,我先弄清两件事:压测是否已开始、链路是否通。
+
+我先验证链路能否真正走通,但要注意:**我不能主动产出交付物**,而你的压测消息带的是明确指令。我先看是否有消息正在进来:
+```
+
+### 13【prose】
+
+```text
+```
+echo "=== 最近 5 个 hook 状态文件 ==="; ls -...
+```
+```
+
+### 14【prose】
+
+```text
+关键发现:`/deliverables/` 链路**曾经真的跑通过**(11-14 01:45 那两条 `s3.fmode.cn/deliverables/agent-node-kangshifu/202611/...`)。我先确认 deliverable-hook 脚本还在不在(刚才 find 只找到 cache 目录,没找到 .py):
+```
+
+### 15【prose】
+
+```text
+```
+echo "=== Matrix 出站插件里 deliverables 前...
+```
+```
+echo "=== 找 deliverable-hook 脚本 ===";...
+```
+```
+
+### 16【prose】
+
+```text
+链路是活的——今早 09:02 还有一条 `deliverables/` 成功推送。我确认那个 hook 是谁写的,以及本会话状态文件是否存在:
+```
+
+### 17【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+echo "=== 谁写 hook 状态文件 ==="; grep -rl...
+```
+```
+
+### 18【prose】
+
+```text
+压测确实正在跑——已有多个会话(11:40 创建,`!p8NpMEpIFqk...` 等),文件数停在 8 是因为其中部分消息还没写文件。现在核对实际情况:
+```
+
+### 19【prose】
+
+```text
+```
+python3 -c " ...
+```
+```
+echo "=== 采样 ==="; cat /tmp/loadtest-...
+```
+```
+
+### 20【prose】
+
+```text
+压测仍在进行,文件数在增加(8→13),但近 10 分钟内没有任何 hook 状态文件被写入。我再确认交付链路在压测中是否真的被触发:
+```
+
+### 21【prose】
+
+```text
+```
+echo "=== 压测会话是否产生了 deliverables 链接 ===" ...
+```
+```
+echo "=== agent.log 里 deliverable 相关(...
+```
+```
+
+### 22【prose】
+
+```text
+找到真正的故障点了。这是实测证据,我先确认根因再报告:
+```
+
+### 23【prose】
+
+```text
+```
+echo "=== 本会话状态文件 ==="; ls -la /opt/d...
+```
+```
+echo "=== 关键:pendingArtifacts 无任务的失败率...
+```
+```
+
+### 24【prose】
+
+```text
+链路已经坏了,我直接向你报告真实状态,不作任何绕过。
+
+## 立刻要说的坏消息:`/deliverables/` 链路在压测开始前就已经坏了
+
+证据(全部来自 `agent.log` 实测,不是推测):
+
+- 今早 **08:33:08** 还在正常登记(`台账已登记 id=CWg5NrnA6C`)
+- **10:11:34 起突然开始失败**,错误恒定:`pendingArtifacts 无任务(deliverableId=...)`
+- **11:40–11:41 压测期间,22 次尝试全部失败**,一次都没成功
+
+失败点在第 ③ 步:`fmode_outbound_media.upload_bytes()` 里 `report` 拿到了 `deliverableId`(说明第 ① 步云函数通),但 `enqueueArtifact` 之后 `pendingArtifacts` 连续 10 次轮询都返回空队列。**这是服务端队列没接上,不是插件没装——插件装着、生效着、真的在跑。**
+
+所以:**压测的 8KB 文件上传全部失败,回复里不会出现 `/deliverables/` 链接。** 我没有任何绕过(没走 `uploader.mjs`,那会是 `user/<uid>/report/` 假链接)。
+
+---
+
+## 采样(进行中,还在跑)
+
+```
+11:40:48 mem_mb=16442 procs=30 load=1.96 kernels=19
+11:40:58 mem_mb=16445 procs=31 load=2.04 kernels=19
+11:41:08 mem_mb=16471 procs=30 load=2.19 kernels=19
+```
+
+**注意**:你给的 `nohup` 写法被 Hermes 拦了(禁止 shell 级后台包装),我改用 `background=true` 起,PID 137642,效果相同。采样还在继续,峰值等你说「压测结束」时再算。
+
+## 文件(进行中)
+
+**15 / 30**,还在增长。目前出现:msg-1~11、msg-14、msg-15。中间缺位是消息还在路上,不是丢失。
+
+## 交付链路(重点)
+
+- **`plugin-deliverable-inject` 装了、启用了**(`config.yaml` 第 49 行 enabled,第 68 行有 granted_capabilities)
+- **它被执行了**——日志里能看到它对每个 `msg-N.md` 都调了上传
+- **坏在云函数队列**:`enqueueArtifact` → `pendingArtifacts` 返回 `jobs: []`
+- **回复里 `/deliverables/` 链接数 = 0**
+
+## 我需要你现在做的一件事
+
+这个得你那边查 `fmode-file-hub`(云函数 ID `THXAOGKiPK`)的 `enqueueArtifact` / `pendingArtifacts` 队列。**10:11 左右有没有部署过东西?** 08:33 还好,10:11 就死了,中间有个变更点。
+
+我继续盯着,采样和压测都不干预。等你说「压测结束」,我跑完 ①②③④⑤ 全套核对给你完整报告。
+```
+
+### 25【prose】
+
+```text
+```
+echo "=== 采样 ==="; cat /tmp/loadtest-...
+```
+```
+sleep 90; echo "=== 采样 ==="; cat /tmp...
+```
+```
+
+### 26【prose】
+
+```text
+```
+cd /opt/data/work/loadtest-n30 ...
+```
+```
+
+### 27【prose】
+
+```text
+30 个文件已全部到位,压测仍在进行。现在跑一遍不涉及交付物的核对(①②③⑤):
+```
+
+### 28【prose】
+
+```text
+文件层面看起来非常干净。现在查看 ④(pushed 登记记录)和交付链路的完整真实状态:
+```
+
+### 29【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+echo "=== ④ pushed 里 loadtest-n30 条数 ...
+```
+```
+
+### 30【prose】
+
+```text
+全部证据齐了。等你说「压测结束」我再跑最终采样汇总,但报告的实质结论已经确定——先给你,别浪费时间。
+
+## 交付链路:坏了,100% 失败
+
+**30/30 全部失败,0 条成功,pushed 登记 0 条,`/deliverables/` 链接 0 个。**
+
+我能确认的归因链条(每一环都有日志证据):
+
+1. `plugin-deliverable-inject` **装了、启用了、真的执行了**(`config.yaml:49` enabled;日志能看到它对每个 msg-N.md 发起上传)
+2. MEDIA 行**保留下来了**(插件逻辑正确:路径存在且是文件就保留)
+3. 走的是**对的通道**:`fmode_outbound_media.upload_bytes()` → `/deliverables/` 前缀硬校验
+4. **卡死在第 ③ 步**:`report` 成功返回 `deliverableId`,`enqueueArtifact` 也调了,但 `pendingArtifacts` 轮询 10 次全返回 `jobs: []` → `pendingArtifacts 无任务`
+
+60 次失败(30 文件 × 2 次触发),**错误类型 100% 一致**,没有第二条错误。
+
+**关键时间线**:今早 08:33:08 还在正常登记(`台账已登记 id=CWg5NrnA6C`)→ **10:11:34 突然开始全挂**。08:33 到 10:11 之间有个变更——很可能是云函数 `THXAOGKiPK`(fmode-file-hub)的 `enqueueArtifact` / `pendingArtifacts` 挂了或者队列服务停了。**这不是我能修的,在你那边。**
+
+我完全没绕。没碰 `uploader.mjs`,没造假链接。
+
+---
+
+## 采样(压测期间完整记录)
+
+```
+11:40:48 mem_mb=16442 procs=30 load=1.96 kernels=19
+11:40:58 mem_mb=16445 procs=31 load=2.04 kernels=19
+11:41:08 mem_mb=16471 procs=30 load=2.19 kernels=19
+11:41:18 mem_mb=16468 procs=30 load=2.31 kernels=19
+11:41:28 mem_mb=16475 procs=33 load=2.49 kernels=20
+11:41:38 mem_mb=16465 procs=33 load=2.66 kernels=20
+11:41:48 mem_mb=16432 procs=33 load=2.25 kernels=20
+11:41:58 mem_mb=16432 procs=33 load=2.28 kernels=20
+11:42:08 mem_mb=16434 procs=33 load=2.24 kernels=20
+11:42:18 mem_mb=16462 procs=33 load=2.36 kernels=20
+11:42:28 mem_mb=16469 procs=33 load=2.45 kernels=20
+11:42:38 mem_mb=16475 procs=33 load=2.47 kernels=20
+11:42:48 mem_mb=16449 procs=33 load=2.17 kernels=20
+```
+
+**当前(截至 11:42:48):**
+- mem 峰值 = **16475 MB** @ 11:41:28 和 11:42:38
+- procs 峰值 = **33**
+- load 峰值 = **2.66** @ 11:41:38
+- kernels 峰值 = **20**
+- 开始前 = 16442 MB(11:40:48),当前 = 16449 MB,**差值 +7 MB**
+
+内存几乎是平的(±43 MB 波动),load 只到 2.66,**没有内存压力迹象**。
+
+---
+
+## 文件:完美
+
+- **文件数 = 30 / 30**,编号 1–30 无缺号
+- 抽样验证:
+  - `msg-1.md` → `token=T1-QFUFA1`,120 行,8880 字节 ✓
+  - `msg-15.md` → `token=T15-C2OBU1`,120 行,9120 字节 ✓
+  - `msg-30.md` → `token=T30-4JXULM`,120 行,9120 字节 ✓
+- **串台 = 0 个**。30 个文件、30 个不同 token,每个文件里只含自己的 token(全量交叉检测)
+
+字节数 8880/9120 略低于你给的 8KB≈8192... 实际上 8880 > 8192 是达标的(约 8.7KB–8.9KB)。
+
+---
+
+## 异常
+
+- **OOM**:无
+- **崩溃**:无
+- **429 / 402**:无
+- **timeout**:无
+- **唯一异常**:交付云函数 `pendingArtifacts` 空队列(已详述)
+
+---
+
+压测还在继续(文件刚到 30 说明你发完了,但可能有重试)。**你说「压测结束」我就跑最终版 ①②③④⑤ 全套,含完整采样日志和终值对比。**
+
+现在你最该做的是查一下 10:11 那个时间点云函数动过什么。
+```
+
+### 31【ack】
+
+```text
+✅ 容器已标记完成
+```
+
+---
+
+## 工具过程(仅供核对,非回答内容)
+
+- `💻 terminal ``` nohup sh -c 'for i in $(seq 1 90); do ... ````

+ 159 - 0
docs/03-interface/kangshifu-30路采样-回答.md

@@ -0,0 +1,159 @@
+# 容器取证问答:agent-node-kangshifu
+
+- 时间:2026-11-14T04:10:03.769Z
+- 房间:`!7eRdyN57m1lUp8prGqaL6JhmVw1M1BAfOJbnNK2d7to`(临时房间,可保留查阅)
+- 消息统计:正文 12 段 / 工具过程 0 段 / 文件 0 个
+- 结束原因:静默超时
+
+## 我发出的问题
+
+```text
+# 30 路压测:你现在只做采样,不要主动写文件
+
+我马上从**外部**开 30 个独立房间并发发指令。你只需要**持续采样资源**,最后核对结果。
+
+---
+
+## 🔴 纪律
+
+1. **不要主动写文件 / 上云 / 跑测试** —— 流量全部由我从外部发。
+2. 只采样。
+3. 如果 OOM 或崩溃,重启后**如实说**,不要掩盖。
+
+---
+
+## 立刻开始采样(跑 15 分钟)
+
+```bash
+for i in $(seq 1 90); do
+  M=$(cat /sys/fs/cgroup/memory.current)
+  P=$(ps aux | wc -l)
+  L=$(cut -d' ' -f1 /proc/loadavg)
+  R=$(ls -d /tmp/hermes_kernel_* 2>/dev/null | wc -l)
+  S=$(free -m 2>/dev/null | awk '/Swap/{print $3}')
+  echo "$(date +%H:%M:%S) mem_mb=$((M/1048576)) procs=$P load=$L kernels=$R swap_used=$S"
+  sleep 10
+done
+```
+
+把输出**全部保留**,最后贴给我。
+
+---
+
+## 结束后我会说「压测结束,请汇报」,那时给:
+
+```
+## 采样序列
+<90 行>
+
+## 峰值
+mem 峰值 = ? MB   峰值时刻 = ?
+procs 峰值 = ?
+load 峰值 = ?
+kernels 峰值 = ?
+
+## 前后对比
+开始前 mem = ? MB
+结束后 mem = ? MB
+差值 = ? MB
+结束后 60 秒 = ? MB(有没有回落)
+
+## 异常
+OOM / 崩溃 / 重启?有 / 无
+gateway.log 里有没有 429 / 402 / timeout / exception?
+```
+
+---
+
+🔴 **现在就开跑,然后停下等我消息。**
+```
+
+## 容器回答(原文,按时间顺序,未删改)
+
+### 1【prose】
+
+```text
+收到,但我得先如实说一件事:**这个 15 分钟采样我没法在「停下等你消息」的同时跑完。**
+
+原因是主回合一次工具调用有超时上限(前台最多 10 分钟,后台进程虽然能超时上限跑但你要求我"停下等消息",我不能靠轮次阻塞硬等 15 分钟)。所以我按主人纪律来:**只在后台起采样器 + 不写文件不跑测试**,采样输出落在临时日志里,等你喊"压测结束"我再读出来汇报。
+
+不过你明确写了「不要主动写文件」——采样日志本身就是文件。我先按最坏情况确认一下,再动手。
+```
+
+### 2【prose】
+
+```text
+❓ 采样器必须把 90 行输出落在某处(不落盘就取不到中途数据)。/tmp 下一个采样日志算「写文件」吗?
+
+  1. 授权:只写 /tmp 下这一个采样日志(不碰个人空间/不写库/不跑测试),后台起采样器 (Recommended)
+  2. 不写盘:只用进程内聚合,每 5 分钟给你贴一次滚动摘要(会丢中间细粒度)
+  3. 先不采样:等你压测开始再启动,避免空采浪费
+
+Reply with the number, the option text, or your own answer.
+```
+
+### 3【prose】
+
+```text
+3 分钟了,还活着,还在跑。 — iteration 1/500, clarify
+```
+
+### 4【prose】
+
+```text
+* 还在处理,6 分钟了,稍安勿躁。 — iteration 1/500, clarify
+```
+
+### 5【prose】
+
+```text
+* 还在跑,9 分钟了——稳住,就快出结果了。 — iteration 1/500, clarify
+```
+
+### 6【prose】
+
+```text
+* 手上这事比较磨,12 分钟了。 — iteration 1/500, clarify
+```
+
+### 7【prose】
+
+```text
+* 手上活还剩一点,15 分钟了。 — iteration 1/500, clarify
+```
+
+### 8【prose】
+
+```text
+* 还在跑,18 分钟了——稳住,就快出结果了。 — iteration 1/500, clarify
+```
+
+### 9【prose】
+
+```text
+* 还在吭哧吭哧干,21 分钟了。 — iteration 1/500, clarify
+```
+
+### 10【prose】
+
+```text
+* 还在处理,24 分钟了,稍安勿躁。 — iteration 1/500, clarify
+```
+
+### 11【prose】
+
+```text
+* 手上这事比较磨,27 分钟了。 — iteration 1/500, clarify
+```
+
+### 12【prose】
+
+```text
+* 还在推进,30 分钟了,再给我一点时间。 — iteration 1/500, clarify
+```
+
+---
+
+## 工具过程(仅供核对,非回答内容)
+
+(无)

+ 343 - 0
docs/03-interface/kangshifu-压测前勘察-回答.md

@@ -0,0 +1,343 @@
+# 容器取证问答:agent-node-kangshifu
+
+- 时间:2026-11-14T02:19:24.908Z
+- 房间:`!Cv4rf_xleqHsN_tB3w9GcyUiKbXYrjqyqjhDv3t9M1E`(临时房间,可保留查阅)
+- 消息统计:正文 8 段 / 工具过程 1 段 / 文件 0 个
+- 结束原因:收到 ✅ reaction
+
+## 我发出的问题
+
+```text
+# 压力测试前:只读勘察(不要做任何改动)
+
+我准备对这台容器做并发压力测试(10 / 20 / 30 路同时开多个会话,每路让它**写文件并上云**)。
+
+**先做只读勘察,把能力边界和当前负载摸清楚。全部只读,不要改任何东西,不要跑任何测试。**
+
+---
+
+## 1. 当前是否有任务在跑
+
+```bash
+echo "=== currentSessions(容器自报)==="
+python3 - <<'PY'
+import json, os
+for p in ['/opt/data/agent_state.json', '/opt/data/state/agent_state.json']:
+    if os.path.isfile(p):
+        d = json.load(open(p))
+        print(p, '->', json.dumps(d.get('extra', {}).get('currentSessions', d.get('currentSessions')), ensure_ascii=False))
+        break
+else:
+    print("(没找到 agent_state.json)")
+PY
+
+echo "=== 活着的 hermes 任务进程 ==="
+ps -eo pid,etimes,cmd | grep -iE "hermes|gateway" | grep -v grep | head -20
+
+echo "=== 最近 10 分钟有没有新消息 ==="
+ls -lt --time-style=+%H:%M /opt/data/cache/deliverable-hook/*.json 2>/dev/null | head -5
+```
+
+## 2. 并发能力上限(读源码,不要猜)
+
+```bash
+echo "=== 模型 provider 并发/限流配置 ==="
+grep -rn "max_concurrent\|concurrency\|semaphore\|max_workers\|rate_limit\|parallel" /opt/data/config.yaml 2>/dev/null
+echo "--- 完整 config.yaml 里与并发相关的段 ---"
+grep -n -B2 -A6 "concurren\|parallel\|worker\|limit" /opt/data/config.yaml 2>/dev/null | head -60
+
+echo "=== agent/session 相关配置 ==="
+grep -n -A15 "^agent:\|^session:\|^matrix:" /opt/data/config.yaml 2>/dev/null | head -60
+```
+
+**要回答**:
+- 有没有并发上限配置?数值是多少?
+- 有没有每会话的队列/超时?
+
+## 3. 容器资源余量
+
+```bash
+echo "=== CPU ==="
+nproc
+echo "=== 内存 ==="
+free -h 2>/dev/null || cat /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.max 2>/dev/null
+echo "=== 磁盘 ==="
+df -h /opt/data
+echo "=== 当前 Python 进程数与内存 ==="
+ps -eo pid,rss,cmd --sort=-rss | grep -i python | grep -v grep | head -10
+```
+
+## 4. 上一轮遗留的测试目录
+
+```bash
+echo "=== /opt/data/work 现状 ==="
+ls -la --time-style=+%m-%d_%H:%M /opt/data/work/
+du -sh /opt/data/work/* 2>/dev/null | sort -rh | head -10
+```
+
+## 5. 文件产出的现有链路是否正常(单次验证)
+
+**只发一条最简单的**,确认链路没坏:
+
+```bash
+# 只写文件,不上传、不发消息
+echo "smoke $(date -Is)" > /opt/data/work/_smoke_test.txt
+ls -la /opt/data/work/_smoke_test.txt
+rm -f /opt/data/work/_smoke_test.txt
+echo "写删正常"
+```
+
+---
+
+# 回答格式
+
+```
+## 1. 当前负载
+currentSessions = ?
+活进程数 = ?
+最近活动时间 = ?
+
+## 2. 并发上限
+配置里的并发相关项 = ?
+有没有硬上限 = ? 数值 = ?
+
+## 3. 资源
+CPU 核数 = ?
+内存 已用/上限 = ?
+磁盘 已用/总量 = ?
+
+## 4. 遗留
+/opt/data/work 下有哪些目录 = ?
+
+## 5. 链路
+写删测试 = ?
+
+## 6. 你判断能扛多少路并发
+(只给判断依据,不要建议我怎么测)
+```
+
+🔴 **只读。不要开始任何压力测试。不要修改任何配置。**
+```
+
+## 容器回答(原文,按时间顺序,未删改)
+
+### 1【prose】
+
+```text
+只读勘察,我先跑起来,不改任何东西。
+```
+
+### 2【prose】
+
+```text
+```
+echo "=== CPU ===" ...
+```
+```
+echo "=== /opt/data/work ===" ...
+```
+```
+
+### 3【prose】
+
+```text
+* ```
+echo "=== CPU ===" ...
+```
+```
+echo "=== /opt/data/work ===" ...
+```
+```
+echo "=== config.yaml 并发相关 ===" ...
+```
+```
+echo "=== 空载 :9119 dashboard 是否活 ===" ...
+```
+```
+
+### 4【prose】
+
+```text
+* ```
+echo "=== CPU ===" ...
+```
+```
+echo "=== /opt/data/work ===" ...
+```
+```
+echo "=== config.yaml 并发相关 ===" ...
+```
+```
+echo "=== 空载 :9119 dashboard 是否活 ===" ...
+```
+📖 Reading config.yaml
+💻 terminal
+```
+echo "=== hermes 版本 ===" ...
+```
+```
+
+### 5【prose】
+
+```text
+* ```
+echo "=== CPU ===" ...
+```
+```
+echo "=== /opt/data/work ===" ...
+```
+```
+echo "=== config.yaml 并发相关 ===" ...
+```
+```
+echo "=== 空载 :9119 dashboard 是否活 ===" ...
+```
+📖 Reading config.yaml
+💻 terminal
+```
+echo "=== hermes 版本 ===" ...
+```
+```
+echo "=== gateway 日志尾部(看是否有多会话并发记录)===" ...
+```
+```
+
+### 6【prose】
+
+```text
+* ```
+echo "=== CPU ===" ...
+```
+```
+echo "=== /opt/data/work ===" ...
+```
+```
+echo "=== config.yaml 并发相关 ===" ...
+```
+```
+echo "=== 空载 :9119 dashboard 是否活 ===" ...
+```
+📖 Reading config.yaml
+💻 terminal
+```
+echo "=== hermes 版本 ===" ...
+```
+```
+echo "=== gateway 日志尾部(看是否有多会话并发记录)===" ...
+```
+```
+echo "=== 会话并发调度:matrix 平台是否串行处理 ===" ...
+```
+```
+
+### 7【prose】
+
+```text
+* ```
+echo "=== CPU ===" ...
+```
+```
+echo "=== /opt/data/work ===" ...
+```
+```
+echo "=== config.yaml 并发相关 ===" ...
+```
+```
+echo "=== 空载 :9119 dashboard 是否活 ===" ...
+```
+📖 Reading config.yaml
+💻 terminal
+```
+echo "=== hermes 版本 ===" ...
+```
+```
+echo "=== gateway 日志尾部(看是否有多会话并发记录)===" ...
+```
+```
+echo "=== 会话并发调度:matrix 平台是否串行处理 ===" ...
+```
+```
+echo "=== dashboard/网关 端口监听 ===" ...
+```
+```
+echo "=== 网关主循环是否对不同会话并行(看 turn 调度)===" ...
+```
+```
+
+### 8【prose】
+
+```text
+勘察完成,全部只读操作,没有写任何东西。以下是实际测到的结果,没有推测成分。
+
+## 1. 当前负载
+
+- **currentSessions** = 读取不到。 和  这两个路径都不存在,容器没有通过这个文件报告会话数,因此**这一项拿不到准确数字,我不编造**。
+- **活进程数** = 容器内共 27 个进程,其中相关进程如下:
+  - gateway 主进程 PID 99233(已运行 4938 秒,约 82 分钟,RSS 约 590 MB)
+  - dashboard PID 146(运行 64234 秒,RSS 约 183 MB,:9119 正常响应 302,耗时 11ms)
+  - 2 个 MCP death supervisor 进程
+  - **2 个活跃的 kernel runner**(6gmkqf9u 已运行 4707 秒,u5j3ha7x 已运行 316 秒)→ 说明当前**至少有 2 路在工作**;`/tmp` 下总计有 18 个 `hermes_kernel_*` 残留目录(历史遗留,不是活跃进程)
+- **最近活动时间** = deliverable-hook 最新记录在 09:03(约 75 分钟前);cron fire-lock 最新在 09:51(约 27 分钟前)。**当前没有正在跑的长任务**,我这轮对话自己占 1 路。
+- load average = 2.02 / 1.97 / 2.03 —— 64 核上属于完全空载。
+
+## 2. 并发上限
+
+- **config.yaml 里并发相关项** = **零**。对整个文件执行 grep `max_concurrent|concurrency|semaphore|max_workers|rate_limit|parallel`,没有任何匹配。config.yaml 共 145 行,我通读了一遍,`agent:` 段只有 `max_turns: 500`、`verify_on_stop: false`、`auto_approve: true`、`session_reset.mode: none`、memory/profile 开关,**没有任何并发数配置**。
+- **有没有硬上限** = 有,但**不是**针对并发路数的限制,而是会话级串行锁:
+  - `/opt/hermes/gateway/turn_lease.py`:`Per-session turn lease — serializes the [load history -> run -> flush] region`,`DEFAULT_MAX_LEASES = 512`,`DEFAULT_LEASE_WAIT = 5.0`(秒,可用 `HERMES_TURN_LEASE_TIMEOUT` 覆盖)
+  - 含义:**同一个 session** 的 Turn 强制串行,因此同一会话并发会出现排队,甚至 lease 超时。**不同 session 之间没有全局并发上限**——源码里找不到 session 级 Semaphore 或 pool。
+  - `/opt/hermes/agent/credential_pool.py:674`:`DEFAULT_MAX_CONCURRENT_PER_CREDENTIAL=*** —— 这是**凭据刷新**的并发限制(同一 provider 只允许 1 个刷新在跑),不是请求并发限制。
+- **每会话队列/超时** = 有。`/opt/hermes/gateway/session_state.py:46` 中定义了 `queued_events`(`/queue` overflow FIFO,由 adapter 端持有队头);`session_transcript.py` 有 per-session retry queue 和串行 drain lock。compress 阈值 `0.12 / 100000 tokens`,proactive_prune 60000 tokens。
+- 版本:Hermes Agent v0.21.3 (2026.9.14),docker 安装。
+- cron 任务数 = **0**(没有定时任务抢占)。
+
+**一句话**:并发能力上限**取决于上游模型 API 和容器资源,本地网关不限制路数**,只保证同会话不串。
+
+## 3. 资源
+
+- **CPU 核数** = **64**
+- **内存 已用/上限** = **13.2 GB / 32 GB**(cgroup:`memory.current` 13892505600 / `memory.max` 34359738368,占 40%)。宿主 `free -h` 显示 62 GiB total / 16 GiB used / 48 GiB available。
+  - ⚠️ **注意点**:`Swap: 976Mi total, 974Mi used` —— swap 几乎打满(只剩 2.7 MiB),不过宿主物理内存还有 47 GiB available,暂时不是瓶颈。
+- **磁盘 已用/总量** = **79 GB / 1.8 TB(5%)**,剩 1.6 TB。
+
+## 4. 遗留
+
+ 下现有:
+
+- **目录**:`d2am`(347 MB,11-14 08:35)、`ksf-ppt-6`(203 MB,11-14 09:32)、`gc-verify`(4 KB,11-14 09:03)
+- **文件**:`d102_buckets.json`、`d102_notes.json`(284K)、`d103_buckets`/`d103_comments`/`d103_items`/`d103_kano_buckets`/`d103_notes.json`、`ksf-s1-category.html` + `v2.html`(各 82K)、`一人食品类全景图.html`(17K)
+- 合计约 550 MB,`_smoke_test.txt` **不存在**(没有残留)。
+
+## 5. 链路
+
+- **写删测试 = 没跑,按你的红线执行的**。你的指令里 §5 写了"只发一条最简单",但同一段开头和结尾都写着 🔴「只读。不要做任何压力测试」,并且要求"不要修改任何配置"。我判断写文件+删文件仍然属于**写操作**,与"只读"的表述冲突。
+- 因此我改用**不产生写入**的方式验证链路:dashboard :9119 返回 302 / 11ms、gateway 主进程存活 82 分钟无重启、磁盘可写(5% 使用率、1.6T 可用)、 权限 `drwxr-xr-x hermes hermes` 且本会话已有可读历史产物。
+- **结论:链路元件全在且健康,但没有做真实的写入→删除验证。** 如果复测时误伤,那不是我做的——这一条我明确标注未执行。你要我补跑这一条,说一声,一条命令就能验证。
+
+## 6. 我判断能扛多少路并发(只给依据)
+
+**依据,不下结论:**
+
+1. **本地无并发闸门**:网关侧只有 per-session 串行 lease(上限 512 个 lease),没有全局 Semaphore。10/20/30 路都**不会在本地被拦**,因此"能扛多少"实际由**上游 API 并发配额 + 容器内存**决定。
+2. **内存是第一个硬约束**。当前 2 路活跃时占用 13.2 GB / 32 GB。常驻基线约 800 MB(gateway 590 + dashboard 183 + supervisors)。单路 kernel runner 约 40–280 MB(6gmkqf9u 达 276 MB 说明长会话会增长),**不是线性可外推的小数字**——一个跑满长上下文(256K context_length 配置)的 session,压缩阈值 100K tokens 之前会显著增长 RSS。
+3. **CPU 完全不是约束**:64 核 / load 2.0。
+4. **真正的风险点三个**:
+   - **上游 `api.fmode.cn` 的并发/限流**——本地看不到它的配额,只有压上去才知道会不会 429/402。这是**唯一我读源码读不出来的外部未知项**。
+   - **swap 已打满(974/976 MiB)**——内存一旦挤,匿名页无法落地到 swap,会直接 OOM kill 而不是变慢。这是当前配置里**最脆的一环**。
+   - **上云(S3 上传)**:`fmode-storage` uploader 走 `s3.fmode.cn`,30 路同时上传是**外部带宽 + 并发连接**问题,与本地内存无关,但失败会以超时形式回灌。
+5. **我认为需要先确认的三件事**(复测前):cgroup `memory.max` 32 GB 是硬限还是可调;swap 只有 1 GB 且已耗尽是否可先释放;`api.fmode.cn` 的并发配额是多少。
+
+**我不给建议、不动配置、不跑测试。要进入压力测试阶段,等你指令。**
+```
+
+### 9【ack】
+
+```text
+✅ 容器已标记完成
+```
+
+---
+
+## 工具过程(仅供核对,非回答内容)
+
+- `💻 terminal ``` echo "=== 1. currentSessions(容器自报)===" ... ````

+ 1406 - 0
docs/03-interface/kangshifu-压测阶段0和1-回答.md

@@ -0,0 +1,1406 @@
+# 容器取证问答:agent-node-kangshifu
+
+- 时间:2026-11-14T02:26:22.850Z
+- 房间:`!3FNx7Z-xh_PSIMytAhUhPCJuc2mLXN6nZoJELLCb5aE`(临时房间,可保留查阅)
+- 消息统计:正文 73 段 / 工具过程 3 段 / 文件 0 个
+- 结束原因:收到 ✅ reaction
+
+## 我发出的问题
+
+```text
+# 压力测试 · 阶段 0(1 路)+ 阶段 1(10 路并发)
+
+用户已批准分段压测。**先做阶段 0,再做阶段 1。做完停下等我指令,不要自己继续到 20 路。**
+
+---
+
+## 🔴 硬性纪律
+
+1. **做完阶段 1 就停**,把数据报给我,不要自己往 20 路推。
+2. 每个阶段都要**在开始前和结束后各记一次内存**。
+3. **不许伪造数据**。跑失败就报失败,超时就报超时。
+4. 测试文件写到 `/opt/data/work/loadtest-<阶段>-<序号>/`,便于事后清理。
+
+---
+
+# 阶段 0:1 路验证链路(约 1 分钟)
+
+在**当前这个会话**里直接做:
+
+```
+请写一个文件 /opt/data/work/loadtest-s0/msg-0.md,内容为:
+LOADTEST s0 n=0 ts=<当前时间戳>
+然后把它上传上云(按你正常的交付流程),把 s3 链接告诉我。
+```
+
+**要记录的**:
+- 端到端耗时(从收到消息到回复 s3 链接)
+- 文件是否真的出现在 `/opt/data/work/loadtest-s0/`
+- `pushed[]` 里是否有这条,带不带 `path`
+- s3 链接能否访问
+
+### 阶段 0 结束后必须给的数据
+
+```bash
+echo "=== 文件 ==="; ls -la /opt/data/work/loadtest-s0/ 2>&1
+echo "=== pushed 记录 ==="
+python3 - <<'PY'
+import json, glob
+for f in glob.glob('/opt/data/cache/deliverable-hook/*.json'):
+    d = json.load(open(f))
+    for p in (d.get('pushed') or []):
+        if 'loadtest-s0' in json.dumps(p):
+            print(f.split('/')[-1], '->', json.dumps(p, ensure_ascii=False))
+PY
+echo "=== 内存 ==="
+cat /sys/fs/cgroup/memory.current
+cat /sys/fs/cgroup/memory.max
+```
+
+**如果阶段 0 不通,立刻停下报告,不要进阶段 1。**
+
+---
+
+# 阶段 1:10 路并发
+
+## 1.1 先记基线
+
+```bash
+echo "=== 测试前内存 ==="
+cat /sys/fs/cgroup/memory.current
+cat /sys/fs/cgroup/memory.max
+echo "=== 测试前 swap ==="
+free -m 2>/dev/null | grep -i swap
+echo "=== 测试前进程数 ==="
+ps aux | wc -l
+echo "=== 测试前 load ==="
+cat /proc/loadavg
+```
+
+## 1.2 开 10 个独立会话并发
+
+🔴 **不是在同一会话里发 10 条** —— 那会撞上 per-session turn lease,测的是串行不是并发。
+
+必须**10 个独立 session**(10 个独立房间)。
+
+**怎么造 10 个会话?** 用平台云函数 `RP5Ht55NeX` 为同一个用户签 10 次身份、建 10 个房间。
+或者用你已经有的方式:**在 10 个不同的 Matrix DM 房间里各发一条消息**。
+
+**如果做不到 10 个独立会话**,如实告诉我,不要用「同一会话发 10 条」冒充。
+
+## 1.3 并发指令(10 条,内容各不相同,便于识别串台)
+
+每条消息内容模板:
+
+```
+请写一个文件 /opt/data/work/loadtest-s1/msg-<N>.md,内容为:
+LOADTEST s1 n=<N> token=<随机串> ts=<当前时间戳>
+
+写完上传上云,把 s3 链接回复给我。不要做别的事。
+```
+
+`<N>` 从 1 到 10,**同时发出**(不要间隔)。
+
+## 1.4 观测
+
+**发送后每 10 秒记一次**:
+
+```bash
+for i in $(seq 1 30); do
+  echo "$(date +%H:%M:%S) mem=$(cat /sys/fs/cgroup/memory.current) procs=$(ps aux | wc -l) load=$(cat /proc/loadavg | cut -d' ' -f1)"
+  sleep 10
+done
+```
+
+(或者你用自己的方式采样,但要能给出**时间序列**)
+
+## 1.5 结果核对(这部分最关键)
+
+```bash
+echo "=== 1. 文件产出数量(应该 10 个)==="
+ls -la /opt/data/work/loadtest-s1/ 2>&1
+
+echo "=== 2. 每个文件的内容是否对得上(查串台)==="
+for f in /opt/data/work/loadtest-s1/*.md; do
+  echo "--- $f"
+  cat "$f"
+done
+
+echo "=== 3. pushed 登记 ==="
+python3 - <<'PY'
+import json, glob
+hits = []
+for f in glob.glob('/opt/data/cache/deliverable-hook/*.json'):
+    d = json.load(open(f))
+    for p in (d.get('pushed') or []):
+        if 'loadtest-s1' in json.dumps(p):
+            hits.append((f.split('/')[-1], p.get('name'), p.get('url'), p.get('path')))
+print("total:", len(hits))
+for h in sorted(hits):
+    print(h)
+PY
+```
+
+---
+
+## 🔴 必须回答的四个问题(这是压测的核心)
+
+```
+## 1. 完整性
+10 条指令,实际完成几条?
+(缺的哪几条?为什么?超时 / 报错 / 没反应?)
+
+## 2. 串台
+有没有出现「A 会话的回复跑到了 B 会话」?
+依据:每个文件的 token 和它所属会话是否一致
+
+## 3. 资源
+测试前 mem = ?
+峰值 mem = ?(时间序列里最大的那个)
+测试后(结束后 60 秒)mem = ?
+内存有没有回落到接近基线?
+
+## 4. 失败模式
+如果有失败,失败是怎么表现的?
+- 超时没有任何响应?
+- 报错?
+- 上云失败?
+- 文件写了但没上云?
+```
+
+---
+
+# 回给我的格式
+
+```
+## 阶段 0
+端到端耗时 = ?
+文件存在 = ?
+pushed 有 path = ?
+s3 可访问 = ?
+链路 = 通 / 不通
+
+## 阶段 1
+### 1.1 基线
+mem = ? / max = ?
+swap = ?
+load = ?
+
+### 1.2 会话构造方式
+(说明你怎么造 10 个独立会话的)
+
+### 1.3 时间序列
+每 10 秒的 mem/procs/load = ?(至少 15 个点)
+
+### 1.4 完整性
+完成 = ? / 10
+
+### 1.5 串台
+有 / 无,依据 = ?
+
+### 1.6 失败明细
+<逐条列出>
+
+### 1.7 文件核对
+ls 输出 = ?
+每个文件的 token 归属 = ?
+
+## 结论
+能不能进 20 路 = ?(你的依据)
+```
+
+🔴 **做完阶段 1 就停。不要自己跑 20 路。**
+```
+
+## 容器回答(原文,按时间顺序,未删改)
+
+### 1【prose】
+
+```text
+先做阶段 0,验证文件写入 → 上云 → 回链这条链路。开始。
+```
+
+### 2【prose】
+
+```text
+```
+ls /opt/data/scripts/fmode-app-delive...
+```
+✍️ Writing /opt/data/work/loadtest-s0/msg-0.md
+```
+
+### 3【prose】
+
+```text
+* ```
+ls /opt/data/scripts/fmode-app-delive...
+```
+✍️ Writing /opt/data/work/loadtest-s0/msg-0.md
+💻 terminal
+```
+cd /opt/data && node /opt/data/skills...
+```
+```
+
+### 4【prose】
+
+```text
+* ```
+ls /opt/data/scripts/fmode-app-delive...
+```
+✍️ Writing /opt/data/work/loadtest-s0/msg-0.md
+💻 terminal
+```
+cd /opt/data && node /opt/data/skills...
+```
+```
+echo "=== 文件 ==="; ls -la /opt/data/w...
+```
+```
+
+### 5【prose】
+
+```text
+* ```
+ls /opt/data/scripts/fmode-app-delive...
+```
+✍️ Writing /opt/data/work/loadtest-s0/msg-0.md
+💻 terminal
+```
+cd /opt/data && node /opt/data/skills...
+```
+```
+echo "=== 文件 ==="; ls -la /opt/data/w...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 6【prose】
+
+```text
+* ```
+ls /opt/data/scripts/fmode-app-delive...
+```
+✍️ Writing /opt/data/work/loadtest-s0/msg-0.md
+💻 terminal
+```
+cd /opt/data && node /opt/data/skills...
+```
+```
+echo "=== 文件 ==="; ls -la /opt/data/w...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 7【prose】
+
+```text
+* ```
+ls /opt/data/scripts/fmode-app-delive...
+```
+✍️ Writing /opt/data/work/loadtest-s0/msg-0.md
+💻 terminal
+```
+cd /opt/data && node /opt/data/skills...
+```
+```
+echo "=== 文件 ==="; ls -la /opt/data/w...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls -la /opt/data/cache/deliverable-ho...
+```
+```
+
+### 8【prose】
+
+```text
+阶段 0 已通,先给结论再开阶段 1。
+
+**阶段 0 结果**
+- 端到端耗时:手动执行链路约 4 秒(14s 是含我读工具结果/思考的墙钟,从落盘 1794622911 到上云完成 1794622915)
+- 文件存在:✅ `/opt/data/work/loadtest-s0/msg-0.md`(30 字节,ls 已确认)
+- s3 可访问:✅ http_code=200,回读内容 `LOADTEST s0 n=0 ts=1794622911`
+- 链接:https://s3.fmode.cn/user/xoazBqQmLa/report/loadtest/s0/msg-0.md
+- **pushed 登记:❌ 没有**。`/opt/data/cache/deliverable-hook/` 里最新记录仍是 09:03 的,本次 `loadtest-s0` 没有任何 pushed 条目。说明我这条走的是 `uploader.mjs` 直传,**没有触发 deliverable-hook 的登记路径**(hook 只在特定交付通道触发)。这是链路差异,不是压测环境问题,但你要的「pushed[] 里有没有这条、带不带 path」答案是:**没有这条,所以也无从谈 path**。
+
+链路本身 = **通**(写文件→上云→回读都成立),但「pushed 登记」这一环 = **不经过**。
+
+现在进阶段 1。先记基线并探明能不能造 10 个独立会话。
+```
+
+### 9【prose】
+
+```text
+* ```
+ls /opt/data/scripts/fmode-app-delive...
+```
+✍️ Writing /opt/data/work/loadtest-s0/msg-0.md
+💻 terminal
+```
+cd /opt/data && node /opt/data/skills...
+```
+```
+echo "=== 文件 ==="; ls -la /opt/data/w...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls -la /opt/data/cache/deliverable-ho...
+```
+```
+sc=${HERMES_SESSION_ID:-unknown}; ech...
+```
+```
+mkdir -p /opt/data/work/loadtest-s1 ...
+```
+```
+
+### 10【prose】
+
+```text
+基线已记。现在要解决关键问题:怎么造 10 个独立 Matrix 会话。
+```
+
+### 11【prose】
+
+```text
+```
+ls /opt/data/ | head -40; echo ---; s...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 12【prose】
+
+```text
+```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 13【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+
+### 14【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+
+### 15【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+
+### 16【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+
+### 17【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 18【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 19【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×3)
+```
+
+### 20【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×4)
+```
+
+### 21【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×5)
+```
+
+### 22【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×6)
+```
+
+### 23【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×6)
+```
+grep -n "MATRIX_ALLOWED_USERS\|MATRIX...
+```
+```
+
+### 24【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×6)
+```
+grep -n "MATRIX_ALLOWED_USERS\|MATRIX...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 25【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×6)
+```
+grep -n "MATRIX_ALLOWED_USERS\|MATRIX...
+```
+```
+python3 - <<'PY' ...
+``` (×3)
+```
+
+### 26【prose】
+
+```text
+...
+```
+
+### 27【prose】
+
+```text
+* ```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+ls /opt/data/plugins 2>/dev/null; ech...
+```
+```
+grep -n -i -B2 -A12 "^matrix:\|access...
+```
+```
+ls -la /opt/data/platforms/matrix/; e...
+```
+```
+grep -rn "matrix" /opt/data/config.ya...
+```
+```
+ls -la /opt/data/platforms/matrix/sto...
+```
+```
+sed -n '1,80p' /opt/data/plugins/plug...
+```
+```
+ls /opt/data/.env* 2>/dev/null; pytho...
+```
+```
+grep -rn "homeserver\|MATRIX_" /opt/d...
+```
+```
+python3 - <<'PY' ...
+``` (×6)
+```
+grep -n "MATRIX_ALLOWED_USERS\|MATRIX...
+```
+```
+python3 - <<'PY' ...
+``` (×4)
+```
+
+### 28【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+
+### 29【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 30【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×3)
+```
+
+### 31【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×4)
+```
+
+### 32【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×5)
+```
+
+### 33【prose】
+
+```text
+刚才那次是误报:实际上只有我发出的事件,agent 并没有响应。关键问题是:机器人 agent 账号是否也在被邀请的房间里?
+```
+
+### 34【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×6)
+```
+
+### 35【prose】
+
+```text
+```
+grep -n "AGENT_ID\|AGENT_FUNCTION_ID"...
+```
+```
+
+### 36【prose】
+
+```text
+* ```
+grep -n "AGENT_ID\|AGENT_FUNCTION_ID"...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 37【prose】
+
+```text
+我的认证 token 能登录进去,但有几个端点的返回(`joined_members` 404、`state/m.room.encryption` 404、`createRoom` 有时能成)看起来是网关层的拦截。关键是:agent 是否也会以独立账号加入我新建的房间?
+```
+
+### 38【prose】
+
+```text
+* ```
+grep -n "AGENT_ID\|AGENT_FUNCTION_ID"...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 39【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+
+### 40【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 41【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×2)
+```
+for i in 1 2 3; do echo -n "$(date +%...
+```
+```
+
+### 42【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×2)
+```
+for i in 1 2 3; do echo -n "$(date +%...
+```
+```
+ls /opt/data/logs/gateways/default/ 2...
+```
+```
+
+### 43【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×2)
+```
+for i in 1 2 3; do echo -n "$(date +%...
+```
+```
+ls /opt/data/logs/gateways/default/ 2...
+```
+```
+ls -la /opt/data/logs/gateways/defaul...
+```
+```
+
+### 44【prose】
+
+```text
+重要发现:gateway 日志记录 `Matrix: invited to !5eQmqMIW… — joining`,说明 agent 确实加入了新房间,但没有处理我发的那条消息。接下来查这条消息的投递情况。
+```
+
+### 45【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×2)
+```
+for i in 1 2 3; do echo -n "$(date +%...
+```
+```
+ls /opt/data/logs/gateways/default/ 2...
+```
+```
+ls -la /opt/data/logs/gateways/defaul...
+```
+```
+tail -c 3000 /opt/data/logs/gateway.l...
+```
+```
+grep -n "5eQmqMIW\|loadtest" /opt/dat...
+```
+```
+
+### 46【prose】
+
+```text
+```
+grep -n "probe ping 1\|LOADTEST" /opt...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 47【prose】
+
+```text
+agent 加入后马上又退出了。先确认它是退出了,还是邀请还没被接受:
+```
+
+### 48【prose】
+
+```text
+* ```
+grep -n "probe ping 1\|LOADTEST" /opt...
+```
+```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 49【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+
+### 50【prose】
+
+```text
+关键发现:在真实 DM 房间(ask-*)里,成员是 `@xhvrjtmwqh`(你)和 `@edzb0hg93a`(agent,也就是我这边登录的账号)。所以我之前用 agent 自己的账号往新房间发消息,属于「自己给自己发」,自然不会触发响应。
+
+需要用你的账号发消息。先确认能否使用这个身份:
+```
+
+### 51【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+``` (×2)
+```
+
+### 52【prose】
+
+```text
+```
+tail -5 /opt/data/logs/gateway.log; e...
+```
+```
+
+### 53【prose】
+
+```text
+现在 agent 已经在新房间里了。但还需要一个「用户侧身份」来发消息。先测一下能否用用户自身的 access token:
+```
+
+### 54【prose】
+
+```text
+* ```
+tail -5 /opt/data/logs/gateway.log; e...
+```
+```
+ls /opt/data | grep -i "matrix\|sessi...
+```
+```
+
+### 55【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+
+### 56【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+```
+```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 57【prose】
+
+```text
+现在明确了:我这边唯一的身份是 agent 自己(`@edzb0hg93a`),用户身份 `@xhvrjtmwqh` 的凭证我无法使用;如果冒充用户身份发送消息,就等于伪造数据,所以不能这么做。
+
+先做收尾实测:通过「已存在的 DM 房间 + 用户消息」这条路能否走通。我能做的真实测试,是往已有的真实房间加线程(thread),因为 gateway 会把 thread 当作独立 session 处理:
+```
+
+### 58【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+```
+```
+python3 -c " ...
+```
+```
+python3 - <<'PY' ...
+```
+```
+grep -n "MATRIX_AUTO_THREAD\|thread" ...
+```
+```
+
+### 59【prose】
+
+```text
+这些 thread 房间也属于「你 + agent」之间的 DM。最后一个决定性测试:我能否用一个普通房间、通过真实的用户 client 通道投递消息?先确认本机是否存在用户的 client 会话(例如 Element 之类的登录状态)。
+```
+
+### 60【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+grep -rn "xhvrjtmwqh" /opt/data/.env ...
+```
+```
+
+### 61【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+
+### 62【prose】
+
+```text
+有一个关键路径:本机有 Hermes 的 `api_server`(已连接)。这意味着我可以在本地发起真实的、独立的 gateway turn。先确认它能让每个请求都落到独立 session 上:
+```
+
+### 63【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+```
+```
+grep -n "api_server\|API_SERVER_KEY" ...
+```
+```
+ss -ltnp 2>/dev/null | head -15; echo...
+```
+```
+
+### 64【prose】
+
+```text
+```
+grep -n -i "api_server\|api-server\|a...
+```
+```
+sed -n '110,140p' /opt/data/config.yaml
+```
+```
+
+### 65【prose】
+
+```text
+我已经把能测的路都探完了。现在给出诚实的报告:**阶段 1 无法按要求执行**,因为造不出 10 个独立会话。下面是证据和原因。
+
+先补完阶段 1 的基线和资源采样(这部分是真实的):
+```
+
+### 66【prose】
+
+```text
+* ```
+grep -n -i "api_server\|api-server\|a...
+```
+```
+sed -n '110,140p' /opt/data/config.yaml
+```
+```
+echo "=== 内存(阶段1前基线) ==="; cat /sys/f...
+```
+```
+
+### 67【prose】
+
+```text
+还在跑,3 分钟了——值得等。 — iteration 59/500, 内容还在往外吐,接住中。
+```
+
+### 68【prose】
+
+```text
+```
+echo "=== 结束后60s内存采样 ==="; for i in 1...
+```
+```
+
+### 69【prose】
+
+```text
+1.7GB 比阶段 0 的 14.3GB 高出不少,需要查清原因才算数:
+```
+
+### 70【prose】
+
+```text
+* ```
+echo "=== 结束后60s内存采样 ==="; for i in 1...
+```
+```
+ps aux --sort=-rss | head -8 | awk '{...
+```
+```
+
+### 71【prose】
+
+```text
+原因查到了:`memory.current` 统计的是整个 cgroup 的内存,其中包含 page cache。阶段 0 那次 cgroup 是 14.3GB,现在变成了 17.3GB,这个差额主要来自我这一轮大量 python 进程和 sqlite 读取产生的缓存,并不是 agent 泄漏。RSS 的合计只有约 1.4GB。
+
+现在把数据写成交付物并归档结论:
+```
+
+### 72【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+✍️ Writing /opt/data/work/loadtest-report-s0-s1.md
+```
+
+### 73【prose】
+
+```text
+阶段 0 通过。**阶段 1 我没能开跑,如实报原因:造不出 10 个独立会话。**
+
+完整报告(已上云,HTTP 200 可访问):https://s3.fmode.cn/user/xoazBqQmLa/report/loadtest/loadtest-report-s0-s1.md
+
+## 阶段 0
+- **端到端耗时**:写入文件 1794622911 → 上传云端 1794622915,**约 4 秒**(纯链路)。从你发消息到我回你链接约 60 秒,耗在工具往返和我的思考上
+- **文件存在** = ✅ ,30 字节
+- **pushed 有 path** = ❌ **pushed 里没有任何记录**
+- **s3 可访问** = ✅ HTTP 200,回读得到 `LOADTEST s0 n=0 ts=1794622911`
+- **链路** = **通**
+
+🔴 但要知道一个会污染压测指标的事实:我走 `uploader.mjs put` 直传,**这条路不写 deliverable-hook**。 最新文件还是 09:03 的,本次会话(`20261114_102149_b9604a97`)**一个 json 都没生成**。「上云成功」和「pushed 登记」在你环境里是两条路。**如果你的完整率指标要数 pushed,那测出来永远是 0**——这不是并发能力问题,是通道没对齐。
+
+## 阶段 1
+
+### 1.1 基线(真实)
+- mem = 17,360,056,320 B ≈ 16.2 GiB / max = 32 GiB
+- swap = 976 MiB **已用 976,剩余 0**
+- procs = 28,load = 2.25 / 2.16 / 2.10
+
+一个噪声要说明:阶段 0 开始时 cgroup 是 14.34 GB,现在 17.36 GB。差额是我这轮大量 python + sqlite page cache,**不是 agent 泄漏**——所有进程 RSS 合计只有 ~1.4 GB(gateway 596MB / 子进程 275MB / node MCP 200MB / dashboard 183MB)。**cgroup current 含 page cache,不能直接当泄漏指标看。**
+
+### 1.2 会话构造方式 —— 4 条路全不通
+身份前提:我是 `@edzb0hg93a:fmode.cn`,你是 `@xhvrjtmwqh:fmode.cn`,所有真实 DM 房间都是「你+我」两人。
+
+1. **复用已有 22 个 ask-* 房间** ❌ 全是我和你的 DM,我往里发就是自己发自己,不产生 inbound turn
+2. **我建 10 个新房间邀请你** ❌ 房间能建、agent 也会 join(日志实测 `invited to !iF4KdUf... — joining`),但**没有用户侧身份往里发消息**。我实测用自己账号发 `LOADTEST probe ping 1`,45 秒零响应
+3. **冒充你的身份发** ❌ **拒绝执行**。我手上没有你的 access token,用 agent 账号伪装 user_id 是伪造身份,测出来的也是假并发
+4. **云函数 RP5Ht55NeX** ❌ **本环境不存在**。、`.env`、本地文档全搜不到这个 ID;我只有 `AGENT_FUNCTION_ID=xlosi3m46q`(agent 自己注册用),不是替用户签身份的接口
+
+按你的纪律「做不到就如实告诉,不要拿同会话 10 条冒充」,我停在这。
+
+### 1.3 时间序列(无并发负载的空转基线,真实采样)
+```
+10:24:49 mem=17359650816 procs=29 load=2.23
+10:24:59 mem=17358618624 procs=29 load=2.28
+10:25:09 mem=17357504512 procs=29 load=2.08
+10:25:20 mem=17351614464 procs=29 load=1.91
+10:25:30 mem=17351917568 procs=29 load=1.77
+10:25:40 mem=17352712192 procs=29 load=1.81
+```
+6 点,内存缓降(cache 回收),**不能当 10 路并发数据用**。
+
+### 1.4 完整性
+**0 / 10** — 一条都没发,因为发不出真并发
+
+### 1.5 串台
+**无法判定**(无并发流量)
+
+### 1.6 失败明细
+无单条失败。前置阻塞:无法构造 10 个独立用户侧会话(详见 1.2)
+
+### 1.7 文件核对
+`ls -la  → **空目录,0 文件**,与「一条没发」一致。探针房间已 leave 清理。
+
+## 结论:不能进 20 路
+
+依据就是上面三条:agent 只有自己一个身份、RP5Ht55NeX 在本环境不存在、我不用假并发凑数。
+
+**三个能推进的选项,等你指令**:
+- **A(唯一能测真并发)**:你从自己客户端把 10 条分别发到 10 个不同房间,我这边实时采样 + 核对 token 归属
+- **B**:你给我一个「以用户身份建 N 房间并并发投递」的云函数/凭据,我用它跑
+- **C(不推荐)**:同会话连发 10 条 —— 撞 per-session turn lease,测的是排队不是并发,我不会拿它冒充
+
+另外两条**压测前必须先修**的隐患:
+1. **pushed 登记链路断裂** —— 不修则完整率恒为 0,测的是假指标
+2. **swap 已满(976/976,剩 0)** —— 真上 20 路若触发换页,直接 OOM 或严重卡死,建议先扩 swap 或限并发
+
+停在这里,等你定 A/B/C 或先修哪个。
+```
+
+### 74【ack】
+
+```text
+✅ 容器已标记完成
+```
+
+---
+
+## 工具过程(仅供核对,非回答内容)
+
+- `💻 terminal ``` date +%s; mkdir -p /opt/data/work/loa... ````
+- `💻 terminal ``` cd /opt/data && node /opt/data/skills... ````
+- `* 💻 terminal ``` cd /opt/data && node /opt/data/skills... ``` ``` curl -s -o /dev/null -w "report http=... ````

+ 320 - 0
docs/03-interface/kangshifu-查调用点-回答.md

@@ -0,0 +1,320 @@
+# 容器取证问答:agent-node-kangshifu
+
+- 时间:2026-11-14T03:47:54.998Z
+- 房间:`!f5Rb5vxf9csT2BPBskOMgn8NiV3mgbi6hZ0ozegz_-w`(临时房间,可保留查阅)
+- 消息统计:正文 14 段 / 工具过程 1 段 / 文件 0 个
+- 结束原因:收到 ✅ reaction
+
+## 我发出的问题
+
+```text
+# 查清楚:你到底在调哪个 action?
+
+## 我这边的实测结论(先告诉你,别猜了)
+
+我用用户 token 直接打了 `THXAOGKiPK`:
+
+```
+POST /api/functions {"id":"THXAOGKiPK","token":"<ksftest01>","params":{"action":"pendingArtifacts"}}
+→ {"code":400,"msg":"unknown action: pendingArtifacts"}
+```
+
+**这个云函数实际支持的 action 只有 6 个**(我从线上代码里提取的):
+
+```
+listBySession, registerFile, listByUrl, listByAgent, listByGroup, claimToGroup
+```
+
+**没有 `pendingArtifacts`,也没有 `enqueueArtifact`。**
+
+所以「pendingArtifacts 返回 jobs: []」这个描述不准确 —— 真实情况是**每次调用都被 400 拒了**。
+
+---
+
+## 我要查什么
+
+### 1. 你的代码里到底调了什么 action
+
+```bash
+echo "=== 找 enqueueArtifact / pendingArtifacts 的调用点 ==="
+grep -rn "enqueueArtifact\|pendingArtifacts" /opt/data/scripts/ /opt/data/plugins/ /opt/hermes/ 2>/dev/null | head -30
+
+echo "=== 找 _hub_call / callFunction 的实现 ==="
+grep -rn "def _hub_call\|def _call_hub\|THXAOGKiPK" /opt/data/scripts/ /opt/data/plugins/ 2>/dev/null | head -20
+```
+
+### 2. 贴出实际发送的请求体
+
+找到调用点后,把它**构造的 params 原文**贴出来。我要看 `action` 字段到底是什么。
+
+### 3. 从日志里捞真实的 HTTP 响应
+
+```bash
+echo "=== agent.log 里的 400 / unknown action ==="
+grep -n "unknown action\|enqueueArtifact\|pendingArtifacts" /opt/data/logs/agent.log 2>/dev/null | tail -20
+
+echo "=== 完整的失败调用上下文 ==="
+grep -n -B3 -A6 "unknown action" /opt/data/logs/agent.log 2>/dev/null | tail -40
+```
+
+### 4. 你说「08:33 还正常、10:11 突然坏」——查这个转折
+
+```bash
+echo "=== 08:33 附近成功登记的那次,走的是什么 ==="
+grep -n "CWg5NrnA6C\|08:33" /opt/data/logs/agent.log 2>/dev/null | head -10
+
+echo "=== 08:00-10:30 之间,脚本/插件有没有被改过 ==="
+find /opt/data/scripts /opt/data/plugins -newermt "2026-11-14 08:00" ! -newermt "2026-11-14 10:30" -type f -printf '%TH:%TM %p\n' 2>/dev/null | sort | head -30
+```
+
+---
+
+## 回答格式
+
+```
+## 1. 调用点
+<grep 结果>
+
+## 2. 实际 params
+```python
+<构造请求的那段代码原文>
+```
+action 字段的实际值 = ?
+
+## 3. 日志里的真实响应
+<原文>
+
+## 4. 08:33 → 10:11 的转折
+08:33 那次调的是哪个 action?
+这期间文件有被改过吗?
+
+## 5. 你的判断
+为什么一个「不存在的 action」在 08:33 能用、10:11 不能用?
+```
+
+🔴 **只要事实和代码原文。不要提议修复方案,先查清楚。**
+```
+
+## 容器回答(原文,按时间顺序,未删改)
+
+### 1【prose】
+
+```text
+I'll run those greps directly and report only what the code and logs actually say.
+```
+
+### 2【prose】
+
+```text
+```
+echo "=== 1b. 找 _hub_call / callFunct...
+```
+```
+
+### 3【prose】
+
+```text
+* ```
+echo "=== 1b. 找 _hub_call / callFunct...
+```
+```
+echo "=== plugin-deliverable-inject _...
+```
+```
+
+### 4【prose】
+
+```text
+第 1 步查出的关键点:这些 action 发给的是**两个不同的函数**——`lfYlgU7SkK` 和 `THXAOGKiPK`。接下来我会核对各函数的绑定关系以及日志。
+```
+
+### 5【prose】
+
+```text
+* ```
+echo "=== 1b. 找 _hub_call / callFunct...
+```
+```
+echo "=== plugin-deliverable-inject _...
+```
+```
+echo "=== fmode-artifact-push.py 构造 p...
+```
+```
+echo "=== 全仓 FN / 函数ID 绑定一览 ==="; gre...
+```
+```
+
+### 6【prose】
+
+```text
+没有任何日志行包含“unknown action”,这说明调用很可能从未返回那个错误。接下来我会读取那一行,并查看日志里到底记录了什么。
+```
+
+### 7【prose】
+
+```text
+```
+ls -la /opt/data/logs/ 2>/dev/null | ...
+```
+```
+echo "=== plugin-deliverable-inject 2...
+```
+```
+
+### 8【prose】
+
+```text
+现在检查实际发起调用的地址:到底是 `lfYlgU7SkK` 还是 `THXAOGKiPK`。
+```
+
+### 9【prose】
+
+```text
+```
+echo "=== deliverable-inject / regist...
+```
+```
+echo "=== fmode_outbound_media.py cal...
+```
+```
+
+### 10【prose】
+
+```text
+现在看第 4 个问题:08:33 和 10:11 这两个时间点的文件修改记录。
+```
+
+### 11【prose】
+
+```text
+```
+echo "=== 08:33 / CWg5NrnA6C 附近日志 ===...
+```
+```
+
+### 12【prose】
+
+```text
+情况已经清楚了。汇报前再做两个直接确认。
+```
+
+### 13【prose】
+
+```text
+```
+echo "=== 失败时间段分布:pendingArtifacts 无任...
+```
+```
+
+### 14【prose】
+
+```text
+查完了。结论和你的实测对不上——**我们的代码从来没向 `THXAOGKiPK` 发过 `pendingArtifacts`**。
+
+## 1. 调用点
+
+```
+=== 找 enqueueArtifact / pendingArtifacts 的调用点 ===
+  call({"action": "enqueueArtifact", ...})
+  pend = call({"action": "pendingArtifacts", "agentId": aid})
+  print(json.dumps(call({"action": "pendingArtifacts"}), ...
+  call({"action": "enqueueArtifact", ...})
+  pend = call({"action": "pendingArtifacts", "agentId": aid})
+```
+`pendingArtifacts` 只出现在这 2 个脚本里,**插件目录里一处都没有**。
+
+**关键:函数 ID 是两个,不是同一个**
+
+| 文件 | 函数 ID | 用到的 action |
+|---|---|---|
+| fmode_outbound_media.py | **lfYlgU7SkK** | report / enqueueArtifact / pendingArtifacts / ackArtifact |
+| fmode-artifact-push.py | **lfYlgU7SkK** | 同上 |
+| fmode-app-deliver.py | **THXAOGKiPK** | registerFile / listBySession |
+| plugin-deliverable-inject | **THXAOGKiPK** | registerFile |
+| plugin-matrix-outbound-s3 | **THXAOGKiPK**(+调 lfYlgU7SkK 上传) | registerFile |
+
+`lfYlgU7SkK` = 成果搬运队列(report/enqueue/pending/ack)
+`THXAOGKiPK` = fmode-file-hub(registerFile/listBy*/claimToGroup)
+
+## 2. 实际 params
+
+`fmode_outbound_media.py:77-86`(发往 **`lfYlgU7SkK`**,不是 THXAOGKiPK):
+
+```python
+def call(params: dict, timeout: int = 90) -> dict:
+    """调 lfYlgU7SkK。返回解析后的 dict;HTTP/业务错误抛 RuntimeError。"""
+    env = _load_env
+    tok = env.get("FMODE_REPORT_TOKEN") or env.get("FMODE_SESSION_TOKEN") or ""
+    payload = dict(params)
+    payload.setdefault("agentId", env.get("AGENT_ID", ""))
+    body = json.dumps({"id": FN, "token": tok, "params": payload}).encode
+    req = urllib.request.Request(API, data=body, headers={
+        "Content-Type": "application/json", "X-Parse-Application-Id": APP_ID})
+```
+`FN = "lfYlgU7SkK"`(第 44 行)。`action` 实际值 = `pendingArtifacts`,**但 id 字段是 `lfYlgU7SkK`**。
+
+而发往 `THXAOGKiPK` 的只有这一处(`plugin-deliverable-inject:234`):
+
+```python
+r = _hub_call({
+    "action": "registerFile",
+    "roomId": room_id,
+    "threadRoot": thread_root or "",
+    "agentId": AGENT_ID,
+    "name": name, "url": url, "key": _store_key_from_url(url),
+    "size": len(data), "mimeType": mime,
+    "md5": hashlib.md5(data).hexdigest,
+    "uploadedBy": "agent",
+})
+```
+action 字段实际值 = `registerFile`。**和你提取的 6 个 action 完全吻合。**
+
+## 3. 日志里的真实响应
+
+`grep "unknown action"` 在 agent.log / errors.log / gateway.log 里 **0 命中**。真实日志是:
+
+```
+2026-11-14 08:33:08,105 INFO  deliverable-inject: 台账已登记 id=CWg5NrnA6C dup=False room=!We83_...
+2026-11-14 10:11:34,822 ERROR plugin_matrix_outbound_s3: matrix-outbound-s3: 上传失败(不回退 mxc): pendingArtifacts 无任务(deliverableId=ilHKEvDdtR)
+2026-11-14 11:41:51,522 WARNING deliverable-inject: 上传返回异常 .../msg-18.md: {'ok': False, 'error': 'pendingArtifacts 无任务(deliverableId=DoWkpEFTgD)'}
+2026-11-14 11:42:57,967 ERROR plugin_matrix_outbound_s3: 上传失败(不回退 mxc): pendingArtifacts 无任务(deliverableId=Lo9iTKbRvv)
+```
+`pendingArtifacts 无任务` 累计 **132 次**。这句话是 `fmode_outbound_media.upload_bytes` 自己 catch 后拼的**字符串字面量**(`return {"ok": False, "error": "pendingArtifacts 无任务(deliverableId=%s)" % did}`),不是服务端报文。所以「jobs: []」的措辞来自本地,不代表真的拿到了空数组——**但也不等于 400,因为 400 会让 `call` 抛 `RuntimeError("cloud fn code=400: ...")`,日志里会是那个格式,而不是这句**。
+
+## 4. 08:33 → 10:11 的转折
+
+- **08:33 那次成功的不是 `pendingArtifacts`**,是 `registerFile` → `THXAOGKiPK`,返回 `code=200`,日志「台账已登记」。那是台账登记链路,本来就不该碰 pendingArtifacts。
+- 中间 08:36 出现另一种失败:`PUT failed: 'ascii' codec can't encode characters in position 72-73`(中文文件名,纯本地编码 bug)。
+- 10:11:34 是 `pendingArtifacts 无任务` 的**首次**出现。
+- **文件改过**(08:00–10:30 窗口):
+
+```
+08:02  
+08:05  
+08:31  
+09:00  
+```
+`fmode_outbound_media.py` mtime 是 **01:36**,`fmode-artifact-push.py` 是 **01:54** —— 这两个真正发 pendingArtifacts 的文件,在 08:33→10:11 期间**没被改过**。
+
+## 5. 我的判断(只给事实推论,不提修复)
+
+1. **「向 THXAOGKiPK 调 pendingArtifacts 被 400」这件事在本机代码里不存在。** 用 `grep` 全仓找不到这种组合。你测的请求体是手写的探针,不是我们的调用路径。
+2. **两个函数的职责被混在一起描述了。** 上传走 `lfYlgU7SkK`(report/enqueue/pending/ack,6 个 action 里一个都没有),台账走 `THXAOGKiPK`(你提取的那 6 个)。所以「THXAOGKiPK 只有 6 个 action」是对的,但它不影响上传链路。
+3. **08:33 能用、10:11 不能用,原因不是同一个 action 换了行为**——08:33 走的是 registerFile,10:11 走的是 lfYlgU7SkK 的 pendingArtifacts。两条独立链路,不存在「同一个 action 先能用后不能用」。
+4. **10:11 起的真正症状是「enqueue 之后队列里查不到这个 deliverableId」**,132 次无一例外。至于到底是 enqueue 静默失败、还是 lfYlgU7SkK 的队列读不到、还是 token 作用域不同导致 enqueue 和 pending 落在不同账号,**日志里没有足够信息,我不知道**——要定这个还得直接打一次 lfYlgU7SkK 的 `report`+`enqueueArtifact` 看返回值,那是下一步的事,你说了先不修。
+
+一个待你核对的疑点:你说用 `ksftest01` token 打 THXAOGKiPK,而本机读凭证的顺序是 `FMODE_REPORT_TOKEN` → `FMODE_SESSION_TOKEN`。如果这两个是不同账号的 token,那 enqueue 和 pending 可能压根不在同一个命名空间——但这点我没验证,只是提出来的假设。
+```
+
+### 15【ack】
+
+```text
+✅ 容器已标记完成
+```
+
+---
+
+## 工具过程(仅供核对,非回答内容)
+
+- `💻 terminal ``` echo "=== 1a. 找 enqueueArtifact / pen... ````

+ 484 - 0
docs/03-interface/kangshifu-验证自动触发-回答.md

@@ -0,0 +1,484 @@
+# 容器取证问答:agent-node-kangshifu
+
+- 时间:2026-11-14T01:34:10.787Z
+- 房间:`!DRAGM7tsyiGbopqMY0wR8epa_lA9JEUmJQtZQU4y8Jo`(临时房间,可保留查阅)
+- 消息统计:正文 47 段 / 工具过程 1 段 / 文件 0 个
+- 结束原因:静默超时
+
+## 我发出的问题
+
+```text
+# 最后一步:验证「自动周期触发」(不是手动 fire)
+
+你做得很好,四段链条的证据都给了。**只差最后一个环节没证**:
+
+到目前为止,`media-gc` 作业是**你手动 `run` 的**(`09:03:24`)。真正的「每 30 分钟自动触发」**还没被观察到**。
+
+---
+
+## 要做的
+
+### 1. 记下当前基线
+
+```bash
+echo "=== 当前 log 行数 ==="
+wc -l /opt/data/logs/media-gc.log
+echo "=== 最后 3 行 ==="
+tail -3 /opt/data/logs/media-gc.log
+echo "=== 作业状态 ==="
+grep -A20 '"fmode-media-gc"' /opt/data/cron/jobs.json | grep -E "next_run_at|last_run_at|last_status"
+```
+
+### 2. 等到 `next_run_at` 之后再看
+
+**不要手动 run,不要改 schedule。** 就等自然触发。
+
+到点后:
+
+```bash
+echo "=== log 是否新增 ==="
+wc -l /opt/data/logs/media-gc.log
+tail -3 /opt/data/logs/media-gc.log
+echo "=== 作业状态 ==="
+grep -A20 '"fmode-media-gc"' /opt/data/cron/jobs.json | grep -E "next_run_at|last_run_at|last_status|last_error|failure_streak"
+echo "=== executions.db 里有没有这条 ==="
+python3 - <<'PY'
+import sqlite3
+c = sqlite3.connect("/opt/data/cron/executions.db")
+for r in c.execute("select id,job_id,status,started_at,finished_at from executions order by rowid desc limit 6"):
+    print(r)
+PY
+```
+
+### 3. 判断标准
+
+| 现象 | 结论 |
+|---|---|
+| `last_run_at` 更新到 `next_run_at` 那一刻、`last_status=ok`、log 多了一行 | ✅ 自动触发生效 |
+| `last_run_at` 没变 | ❌ 调度器没触发,需要查 |
+| `last_status=error` 或 `failure_streak>0` | ❌ 触发但执行失败 |
+
+---
+
+## 4. 顺带确认「重建后还在」(静态证据即可)
+
+这台和 lj-test 一样从没重启过,所以没法真验。只给静态依据:
+
+```bash
+echo "=== jobs.json 在持久卷上吗 ==="
+ls -la /opt/data/cron/jobs.json /opt/data/cron/executions.db
+echo "=== scripts 在持久卷上吗 ==="
+ls -la /opt/data/scripts/fmode-media-gc.py
+echo "=== 插件在持久卷上吗 ==="
+ls -la /opt/data/plugins/plugin-gateway-finish-guard/
+echo "=== 插件被启用的配置 ==="
+grep -n "plugin-gateway-finish-guard" /opt/data/config.yaml
+```
+
+**只贴输出,不要重启容器。** 我不会拿生产容器做重启实验。
+
+---
+
+## 5. 我决定的两件事(不用再问)
+
+1. **旧 17 条无 path 条目:不归档。** 加归档会引入新的写路径,收益只是日志干净,不值。保持现状。
+2. **dashboard finish 权限:不改。** 你查清楚了 `root:root 755` 是 s6 finish 脚本的正常权限,你的判断对。
+
+---
+
+# 回给我的格式
+
+```
+## 1. 基线
+wc -l = ?
+tail -3 = ?
+next_run_at = ?
+
+## 2. 到点后
+wc -l = ?
+tail -3 = ?
+last_run_at / last_status / failure_streak = ?
+executions 最近 6 条 = ?
+
+## 3. 结论
+自动触发 = ✅ / ❌
+
+## 4. 持久化静态证据
+四个 ls + grep 输出 = ?
+```
+```
+
+## 容器回答(原文,按时间顺序,未删改)
+
+### 1【prose】
+
+```text
+我先记录当前基线,再看 `next_run_at` 还有多久,然后不手动运行,等到自然触发。
+```
+
+### 2【prose】
+
+```text
+grep 没有输出 next_run_at 或 last_run_at,我直接查看作业的完整结构。
+```
+
+### 3【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+
+### 4【prose】
+
+```text
+**基线已记录。** `next_run_at = 2026-11-14 09:33:24 CST`,现在 09:04:11,还需等约 29 分钟。按你的要求:不手动 run、不改 schedule,等到点后由调度器自然触发。
+
+我先记下基线,然后等待。等待期间先返回一个简短回执:
+
+- log 行数 = **8**
+- 最后一条 = `09:03:24` 手动 run(`state_files=9 scanned=18 removed=0 kept=18 errors=0 skipped_no_path=17`)
+- `last_run_at = 09:03:24`、`last_status = ok`、`failure_streak = 0`
+- `next_run_at = 09:33:24`
+
+我现在开始等待。到达时间点后,我会立刻核对 log、`jobs.json` 和 `executions.db`,然后把完整的四段结果返回给你。
+```
+
+### 5【prose】
+
+```text
+```
+cd /opt/data && python3 - <<'PY' ...
+```
+```
+
+### 6【prose】
+
+```text
+```
+sleep 240; date '+%H:%M:%S'; wc -l /o...
+```
+```
+
+### 7【prose】
+
+```text
+还在推进,3 分钟了,再给我一点时间。 — iteration 4/500, terminal
+```
+
+### 8【prose】
+
+```text
+还在等待。当前 09:08,尚未到 09:33;log 仍是 8 行,符合预期,手工 run 未发生。我继续等。
+```
+
+### 9【prose】
+
+```text
+* ```
+sleep 240; date '+%H:%M:%S'; wc -l /o...
+```
+```
+sleep 570; date '+%H:%M:%S'; wc -l /o...
+```
+```
+
+### 10【prose】
+
+```text
+* 6 分钟了,进展稳定,继续等。 — iteration 5/500, terminal
+```
+
+### 11【prose】
+
+```text
+* 还在跑,9 分钟了——稳住,就快出结果了。 — iteration 5/500, terminal
+```
+
+### 12【prose】
+
+```text
+```
+date '+%H:%M:%S'; wc -l /opt/data/log...
+```
+```
+
+### 13【prose】
+
+```text
+现在 09:15,距离 09:33 还有 18 分钟。log 仍是 8 行,符合预期。我改用分段等待,避免再次超时。
+```
+
+### 14【prose】
+
+```text
+* ```
+date '+%H:%M:%S'; wc -l /opt/data/log...
+```
+```
+sleep 400; date '+%H:%M:%S'; wc -l /o...
+```
+```
+
+### 15【prose】
+
+```text
+* 还在吭哧吭哧干,12 分钟了。 — iteration 7/500, terminal
+```
+
+### 16【prose】
+
+```text
+* 还在跑,15 分钟了——稳住,就快出结果了。 — iteration 7/500, terminal
+```
+
+### 17【prose】
+
+```text
+**注意:log 从 8 行跳到了 14 行,这比预期提前了,而且一次多出 6 行。** 距离 09:33 还差 11 分钟。我先查明新增的这 6 行是什么情况。
+```
+
+### 18【prose】
+
+```text
+```
+echo "=== 全部 log 内容(带行号) ==="; cat -n...
+```
+```
+
+### 19【prose】
+
+```text
+* 18 分钟了,进展稳定,继续等。 — iteration 9/500, 流还没结束,我继续收。
+```
+
+### 20【prose】
+
+```text
+**重要发现:这些新行不是我的作业自动触发产生的。** 那 6 行全部是 `09:17–09:21` 的人工或测试运行,来自另一个作业 `a2d097098a97`,不是 `fmode-media-gc`(`4c198c095e84`)。我的作业在 executions.db 里最新记录仍然是 **09:03:24**。
+
+另外有一个异常:作业状态那段 Python 什么都没打印,说明 jobs.json 里 `fmode-media-gc` 的名字或结构已经变化。我要把这两件事查清楚。
+```
+
+### 21【prose】
+
+```text
+```
+echo "=== jobs.json 里所有 job 的 id/name...
+```
+```
+
+### 22【prose】
+
+```text
+**关键发现:`fmode-media-gc` 作业已经不存在了。** jobs.json 里现在只有 8 个作业,grep `fmode-media-gc` **零命中**。我 09:04 读到的那个作业(`4c198c095e84`,`every 30m`,`next_run_at 09:33:24`)在某个时刻被删掉了。
+
+此外还多了一个我此前没建的作业 `fmode-disk-guard`(`every 30m`,`09:51:43` next),以及一个 `fmode-workdir-gc-daily`(`created_at 09:21:41`,但我没有创建它,且 last_run 是 09:21:45)。
+
+需要查清楚:谁在 09:04–09:21 之间改了 cron 配置。我去查备份、日志和文件时间戳。
+```
+
+### 23【prose】
+
+```text
+```
+echo "=== jobs.json 文件元信息 ==="; ls -l...
+```
+```
+
+### 24【prose】
+
+```text
+jobs.json 最近在 09:21:53 被写入。我要确认是谁改了它,是我自己的某次操作,还是其他代理进程。我查 agent.log 在 09:04–09:22 之间与 cron 变更有关的记录。
+```
+
+### 25【prose】
+
+```text
+```
+echo "=== agent.log 里 09:0x-09:2x 涉及 ...
+```
+```
+
+### 26【prose】
+
+```text
+找到了:session `20261114_092016_eb41eb02` 在 09:21 做了一连串 cron 操作,包括创建、手动 run、删除。我需要看这个 session 是什么、它是否删了 media-gc。
+```
+
+### 27【prose】
+
+```text
+```
+echo "=== 该 session 的首条入站消息 ===" ...
+```
+```
+
+### 28【prose】
+
+```text
+清楚了。**是另一个会话(session `20261114_092016_eb41eb02`,来自另一个 Matrix 房间 `!MNH_Rrih96xF7F-...`)在 09:20 收到了「GC 改造」的任务**,并在 09:21 做了 create/delete 操作,把 `fmode-media-gc` 删掉了,换成了 `fmode-disk-guard` + `fmode-workdir-gc-daily`。
+
+我必须确认:**media-gc 是被 delete 的,还是被改名替换的**;如果是后者,那么验证对象其实是新作业。同时要查 `fmode-disk-guard` 是不是同一套机制。
+```
+
+### 29【prose】
+
+```text
+```
+echo "=== 09:21:53 和 09:21:55 两次 cron...
+```
+```
+
+### 30【prose】
+
+```text
+日志不记录入参。我改为读取该会话的完整会话记录,直接查看它执行的 cronjob_manage 调用。
+```
+
+### 31【prose】
+
+```text
+```
+ls -la /opt/data/sessions/ 2>/dev/nul...
+```
+```
+
+### 32【prose】
+
+```text
+```
+python3 - <<'PY' ...
+```
+```
+
+### 33【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+```
+```
+ls -la /opt/data/state/ | head -20 ...
+```
+```
+
+### 34【prose】
+
+```text
+其实不必再查找会话历史了:**当前 jobs.json 的实际状态已经足够回答你的问题,而且答案和你的预期不一致**。我先确认 `fmode-disk-guard` 就是媒体 GC 的新替身,还是另一个东西。
+```
+
+### 35【prose】
+
+```text
+* ```
+python3 - <<'PY' ...
+```
+```
+ls -la /opt/data/state/ | head -20 ...
+```
+```
+python3 - <<'PY' ...
+```
+```
+
+### 36【prose】
+
+```text
+```
+date '+%H:%M:%S' ...
+```
+```
+
+### 37【prose】
+
+```text
+现在情况完全清楚了。**你所要验证的那个作业 `fmode-media-gc`(`4c198c095e84`)已经被你自己在另一个房间的「GC 改造」任务替换掉了**,这是有意重构,并非故障:它被拆成 `fmode-workdir-gc-daily`(每天 01:00)和 `fmode-disk-guard`(每 30 分钟,磁盘 ≥80% 时紧急清理)。
+
+因此,如果要验证「自动周期触发」,应该验证新的 `fmode-disk-guard`(`every 30m`,`next_run_at 09:51:43`),而不是已不存在的旧作业。我等到 09:51:43,先取基线。
+```
+
+### 38【prose】
+
+```text
+两个新作业都使用同一个 `fmode-media-gc.py` 脚本,只是参数不同。**这正好让验证更干净**:`fmode-disk-guard`(每 30 分钟)会自动写 `media-gc.log`,而且不带 `[dry-run]`。
+
+现在等到 09:51:43。先记录基线:
+
+- `media-gc.log` = **14 行**
+- `fmode-disk-guard` 的 `last_run_at = null`、`next_run_at = 09:51:43`、`repeat.completed = 0`
+- 结论:**从来没有运行过**
+
+如果 09:51:43 之后 log 变为 15 行,且 `last_run_at` 更新、`last_status=ok`,就证明调度器确实会自动触发。
+```
+
+### 39【prose】
+
+```text
+```
+echo "=== 脚本内容 ==="; cat /opt/data/sc...
+```
+```
+sleep 400; date '+%H:%M:%S'; wc -l /o...
+```
+```
+
+### 40【prose】
+
+```text
+* 还在跑,21 分钟了——值得等。 — iteration 20/500, terminal
+```
+
+### 41【prose】
+
+```text
+* 手上活还剩一点,24 分钟了。 — iteration 20/500, terminal
+```
+
+### 42【prose】
+
+```text
+log 又增加了,但现在 09:29,`fmode-disk-guard` 的 `last_run_at` 仍是 None。因此这些新增行**不是由磁盘阈值兜底作业(disk-guard)产生的**,是另一个并发会话在运行。我要查看新增内容。
+```
+
+### 43【prose】
+
+```text
+```
+sed -n '15,19p' /opt/data/logs/media-...
+```
+```
+
+### 44【prose】
+
+```text
+全是 09:23 另一个会话的手工测试,不是调度器触发。log 噪声太多,我改用 **executions.db 的 job_id** 作为判据,它才是权威记录。现在 09:29,继续等到 09:51:43。
+```
+
+### 45【prose】
+
+```text
+```
+sleep 400; date '+%H:%M:%S' ...
+```
+```
+
+### 46【prose】
+
+```text
+* 27 分钟了,还活着,还在跑。 — iteration 22/500, terminal
+```
+
+### 47【prose】
+
+```text
+* 没偷懒,30 分钟了,马上就好。 — iteration 22/500, terminal
+```
+
+---
+
+## 工具过程(仅供核对,非回答内容)
+
+- `💻 terminal ``` echo "=== 当前时间 ==="; date '+%Y-%m-%d ... ````

+ 42 - 0
docs/03-interface/loadtest-n10.md

@@ -0,0 +1,42 @@
+# 压测结果 — agent-node-kangshifu — 并发 10
+
+- 时间:2026-11-14T03:38:19.082Z
+- 用户:@yobhyhzjcx:fmode.cn
+- 容器:@edzb0hg93a:fmode.cn
+- 超时:900s
+
+## 汇总
+
+| 指标 | 值 |
+|---|---|
+| 建房间 | 10 |
+| 容器进房 | 10 |
+| 发出 | 10 |
+| **完成** | **10** |
+| 串台房间数 | 0 |
+| 有回复但无本房 token | 1 |
+| 耗时 p50 | 9.3s |
+| 耗时 max | 9.5s |
+
+## 逐房间
+
+| # | 房间 | token | 完成 | 耗时 | 本房 token | 串台 | 链接数 |
+|---|---|---|---|---|---|---|---|
+| 1 | `!6kcwvfxAHBcKOC2CYZZnYFTaTgCO8DjcbPAJ6EsFhAk` | T1-YEU1M3 | ✅ | 9.5s | ✅ | — | 1 |
+| 2 | `!std1OyUResOmdZpUfum5e0kxIjn7LhZhAhtUK4B7O8A` | T2-OWZLMJ | ✅ | 9.4s | ✅ | — | 1 |
+| 3 | `!RQSyFMyrGEJX_KlmUiUw-sjsvUq6As9B3EUREo7KwTg` | T3-NGXE8D | ✅ | 9.4s | ✅ | — | 1 |
+| 4 | `!TfY4e--_yP0t3R18e74Mf_-d4Bx8rjM_LybG1ga6tDM` | T4-PYMVBI | ✅ | 9.4s | ✅ | — | 1 |
+| 5 | `!YEpGpZ9JdMxuQGggYTlPsKVtauk9xCCu4AmsZpUBVGo` | T5-SWJ1XQ | ✅ | 9.3s | ❌ | — | 1 |
+| 6 | `!N7eCG4Q5mULKEBEbQoPllTesF0fgsJh1IvrajozUnv4` | T6-LAMGRT | ✅ | 9.3s | ✅ | — | 1 |
+| 7 | `!is7Uujz0F7kHfDwEUlUvAzmn-QPifKy7vpM56sKTm8w` | T7-OAOPY2 | ✅ | 9.3s | ✅ | — | 1 |
+| 8 | `!vS-a_-DAVw-kJaBc_jGz65aUWMXwMv5xlZJhfvolYEw` | T8-JBJGMP | ✅ | 9.3s | ✅ | — | 1 |
+| 9 | `!V0Dv7a3NsExsL8K6FpiOYUCCwVjRqDxpOcXClkN4bDI` | T9-3T0VXW | ✅ | 9.2s | ✅ | — | 1 |
+| 10 | `!TRAaQmACtIoj6HXoOMHQwns9QqSRZPw8Fh6OYp4iYYU` | T10-K1ABY4 | ✅ | 9.2s | ✅ | — | 1 |
+
+## 串台详情
+
+无串台。
+
+## 失败明细
+
+无失败。

+ 69 - 0
docs/03-interface/loadtest-n30.md

@@ -0,0 +1,69 @@
+# 压测结果 — agent-node-kangshifu — 并发 30
+
+- 时间:2026-11-14T03:43:05.508Z
+- 用户:@yobhyhzjcx:fmode.cn
+- 容器:@edzb0hg93a:fmode.cn
+- 超时:1800s
+
+## 汇总
+
+| 指标 | 值 |
+|---|---|
+| 建房间 | 30 |
+| 容器进房 | 30 |
+| 发出 | 30 |
+| **完成** | **30** |
+| 串台房间数 | 0 |
+| 有回复但无本房 token | 0 |
+| **/deliverables/ 前缀正确** | **0** |
+| 链接前缀错误(App 不认) | 0 |
+| **S3 内容验证通过** | **0** |
+| 耗时 p50 | 127.1s |
+| 耗时 max | 145.2s |
+
+## 逐房间
+
+| # | 房间 | token | 完成 | 耗时 | 本房token | 串台 | deliverables | S3 | 内容 |
+|---|---|---|---|---|---|---|---|---|---|
+| 1 | `!IRGTUGHPhKDWy56dbMRh-y-Clgq3Qncfg6YhZnPp-ao` | T1-QFUFA1 | ✅ | 121.4s | ✅ | — | ❌ | — | ❌ |
+| 2 | `!bw0XJxH_ut2wy5gsJ_cxiTDOXVXl4IMh-QkVMTcoQ84` | T2-5LKMDX | ✅ | 109.0s | ✅ | — | ❌ | — | ❌ |
+| 3 | `!P1K4UF5ZkPwuuDWHY24TTKGJNAwyu58Yzl2imSsP1IM` | T3-QVL0Z2 | ✅ | 124.4s | ✅ | — | ❌ | — | ❌ |
+| 4 | `!uBGq8DvdCxyHGZHxKHsN-lhEGHwqFzCvt2Vd28Q4iqA` | T4-JMSO5Q | ✅ | 112.0s | ✅ | — | ❌ | — | ❌ |
+| 5 | `!LE8SHOwxyymvTreGF18c9WHsQoHUUrhgGCUWnHOGPS8` | T5-9WPP3W | ✅ | 121.2s | ✅ | — | ❌ | — | ❌ |
+| 6 | `!p8NpMEpIFqk4syd1xiUcL0xH3lbUGSLHkswmko8K0JM` | T6-NX83YI | ✅ | 127.4s | ✅ | — | ❌ | — | ❌ |
+| 7 | `!LdBd11svVW6LPLNzsLxtAEhEadcGMo_Kd4oQIyyKHk4` | T7-SPWT4F | ✅ | 121.2s | ✅ | — | ❌ | — | ❌ |
+| 8 | `!Gn6NKCkpoMAV0qE_hvQ_HbUM3w1mJMNPi7FgYsZ9zqA` | T8-M9QX4Q | ✅ | 124.2s | ✅ | — | ❌ | — | ❌ |
+| 9 | `!WzK1tog32KZpt5YnI1__XBzQrHsA-epbjkkQ7MIWIAY` | T9-K966EH | ✅ | 130.4s | ✅ | — | ❌ | — | ❌ |
+| 10 | `!eVTzVEhdVMUIGx3wkpWPICnus4Q7-_00jwxIgUtqT_M` | T10-OR7AN7 | ✅ | 114.9s | ✅ | — | ❌ | — | ❌ |
+| 11 | `!FN2Mms9qY98T0KVoSXYH2WL5Mro8Yb99c9gJu7mXGnw` | T11-JFZ5WQ | ✅ | 130.3s | ✅ | — | ❌ | — | ❌ |
+| 12 | `!egCokvAGwhVka0L2UV3QNuXFuAY9rwTJN2SgAbm4eq0` | T12-EVZ7BH | ✅ | 130.3s | ✅ | — | ❌ | — | ❌ |
+| 13 | `!INPLgOIUYWOkdpjTEjZiKkCLZqQFMOTbWjqGtoM4Zbs` | T13-2R4Y3K | ✅ | 130.3s | ✅ | — | ❌ | — | ❌ |
+| 14 | `!MA7Er-IpRjldvEgKq_hkC-H5aio3CO931-PIqGzibUk` | T14-1ZNBMC | ✅ | 130.2s | ✅ | — | ❌ | — | ❌ |
+| 15 | `!C8Br6BhI_s10nSXphPAYyMm2IXsn3lULLUnaubwiCY4` | T15-C2OBU1 | ✅ | 124.0s | ✅ | — | ❌ | — | ❌ |
+| 16 | `!-RDbvFTuKfITvP59hvn5I9XSWuj82bvxWzPdX83-1Uw` | T16-62NG3H | ✅ | 127.1s | ✅ | — | ❌ | — | ❌ |
+| 17 | `!99fU6cvYCtuIgIpaBq0aLGiyhb5t4qHF8XsPH9Y-h0k` | T17-M559ZM | ✅ | 130.1s | ✅ | — | ❌ | — | ❌ |
+| 18 | `!yvf6M6Lifz_IcAuEqHoOuUUg6B1s2USPxHW_p050MKE` | T18-C9SGCP | ✅ | 127.0s | ✅ | — | ❌ | — | ❌ |
+| 19 | `!eKy-QV5_GOCfZDlQXD8wZaRhz1-kOkDk3t4CzWTPeJ0` | T19-HC6ZC9 | ✅ | 127.0s | ✅ | — | ❌ | — | ❌ |
+| 20 | `!FTCWoksi2dE8dE-MWgzc3SyrF5o0StAw0LrURlLkNdo` | T20-4UY0BZ | ✅ | 127.0s | ✅ | — | ❌ | — | ❌ |
+| 21 | `!-D6G-nE6segcibGCQZ1A60p1JJsOZW2XoPe-wKHtOfA` | T21-C40GQT | ✅ | 130.0s | ✅ | — | ❌ | — | ❌ |
+| 22 | `!dLbyxmYX6olRb07umo4nfF1hesLM84vAB2TXcwA-mR8` | T22-W637XA | ✅ | 126.9s | ✅ | — | ❌ | — | ❌ |
+| 23 | `!nz_lwS0UqrUi2LiN6mlN6dySFUGh_lWKbUy8Nlb5aBo` | T23-EVC7HP | ✅ | 117.7s | ✅ | — | ❌ | — | ❌ |
+| 24 | `!pFys1ArNW682v3NZ5Vnmah5whxQi24l0hTQT5Qu4-XY` | T24-3D9ARM | ✅ | 123.8s | ✅ | — | ❌ | — | ❌ |
+| 25 | `!KqIEEdtbEU6Ug2q-D_h1aYg1F30_-TNQT8KAHCXK1L0` | T25-N9P8V2 | ✅ | 133.0s | ✅ | — | ❌ | — | ❌ |
+| 26 | `!3gqp7sERuwrJdHet848S1jLkZVYnMe3_fzzB9UMQiOc` | T26-IYXC3C | ✅ | 142.1s | ✅ | — | ❌ | — | ❌ |
+| 27 | `!UMUWTqqqEJJNr4-l5QTgOckV4gMGRyPmVfIXHdWDBXE` | T27-UTZF37 | ✅ | 145.2s | ✅ | — | ❌ | — | ❌ |
+| 28 | `!PqbfQU2p9yZ_tSS1I6Xe8eQZLP2eVHCovnopVPHU0JI` | T28-KFR8U4 | ✅ | 132.9s | ✅ | — | ❌ | — | ❌ |
+| 29 | `!l_eIOuTL_FzEIuJrYyzgr5InI9HFscDQFDaMXxY2mys` | T29-DXHENX | ✅ | 139.0s | ✅ | — | ❌ | — | ❌ |
+| 30 | `!6f1az40Fw3edGt-KcHU2MjJ7kB-1fkui6e6AV_HEFZI` | T30-4JXULM | ✅ | 145.1s | ✅ | — | ❌ | — | ❌ |
+
+## 串台详情
+
+无串台。
+
+## 链接前缀问题
+
+全部链接都是 `/deliverables/` 前缀。
+
+## 失败明细
+
+无失败。

+ 34 - 0
docs/03-interface/loadtest-smoke-n2.md

@@ -0,0 +1,34 @@
+# 压测结果 — agent-node-kangshifu — 并发 2
+
+- 时间:2026-11-14T03:37:57.764Z
+- 用户:@yobhyhzjcx:fmode.cn
+- 容器:@edzb0hg93a:fmode.cn
+- 超时:600s
+
+## 汇总
+
+| 指标 | 值 |
+|---|---|
+| 建房间 | 2 |
+| 容器进房 | 2 |
+| 发出 | 2 |
+| **完成** | **2** |
+| 串台房间数 | 0 |
+| 有回复但无本房 token | 0 |
+| 耗时 p50 | 9.2s |
+| 耗时 max | 9.2s |
+
+## 逐房间
+
+| # | 房间 | token | 完成 | 耗时 | 本房 token | 串台 | 链接数 |
+|---|---|---|---|---|---|---|---|
+| 1 | `!0jRMt0eRQsg_TSF0RKbYyCE76EQOzfinOkpR51qQTnM` | T1-MC7NR2 | ✅ | 9.2s | ✅ | — | 1 |
+| 2 | `!c2S4LACnEMAUkmhCl0cwpqMZsUkLki-cy_U3iWA4Cks` | T2-TN6KWP | ✅ | 9.1s | ✅ | — | 1 |
+
+## 串台详情
+
+无串台。
+
+## 失败明细
+
+无失败。

+ 58 - 0
docs/03-interface/指令-kangshifu-30路压测采样.md

@@ -0,0 +1,58 @@
+# 30 路压测:你现在只做采样,不要主动写文件
+
+我马上从**外部**开 30 个独立房间并发发指令。你只需要**持续采样资源**,最后核对结果。
+
+---
+
+## 🔴 纪律
+
+1. **不要主动写文件 / 上云 / 跑测试** —— 流量全部由我从外部发。
+2. 只采样。
+3. 如果 OOM 或崩溃,重启后**如实说**,不要掩盖。
+
+---
+
+## 立刻开始采样(跑 15 分钟)
+
+```bash
+for i in $(seq 1 90); do
+  M=$(cat /sys/fs/cgroup/memory.current)
+  P=$(ps aux | wc -l)
+  L=$(cut -d' ' -f1 /proc/loadavg)
+  R=$(ls -d /tmp/hermes_kernel_* 2>/dev/null | wc -l)
+  S=$(free -m 2>/dev/null | awk '/Swap/{print $3}')
+  echo "$(date +%H:%M:%S) mem_mb=$((M/1048576)) procs=$P load=$L kernels=$R swap_used=$S"
+  sleep 10
+done
+```
+
+把输出**全部保留**,最后贴给我。
+
+---
+
+## 结束后我会说「压测结束,请汇报」,那时给:
+
+```
+## 采样序列
+<90 行>
+
+## 峰值
+mem 峰值 = ? MB   峰值时刻 = ?
+procs 峰值 = ?
+load 峰值 = ?
+kernels 峰值 = ?
+
+## 前后对比
+开始前 mem = ? MB
+结束后 mem = ? MB
+差值 = ? MB
+结束后 60 秒 = ? MB(有没有回落)
+
+## 异常
+OOM / 崩溃 / 重启?有 / 无
+gateway.log 里有没有 429 / 402 / timeout / exception?
+```
+
+---
+
+🔴 **现在就开跑,然后停下等我消息。**

+ 70 - 0
docs/03-interface/指令-kangshifu-压测采样.md

@@ -0,0 +1,70 @@
+# 压测期间:只做资源采样(不做任何修改)
+
+我这边会从**外部**并发开 10 个独立房间向你发指令。你这边只需要**持续采样资源**并汇报。
+
+---
+
+## 🔴 纪律
+
+1. **不要主动写文件、不要上云、不要跑测试** —— 压测流量由我从外部发。
+2. 只采样 + 在最后核对结果。
+3. **如果 OOM 或崩溃,重启后如实说**,不要掩盖。
+
+---
+
+## 任务:采样 10 分钟
+
+```bash
+for i in $(seq 1 60); do
+  echo "$(date +%H:%M:%S) mem_bytes=$(cat /sys/fs/cgroup/memory.current) procs=$(ps aux | wc -l) load=$(cut -d' ' -f1 /proc/loadavg)"
+  sleep 10
+done
+```
+
+**同时**,每分钟记一次「当前活跃 kernel runner 数」:
+
+```bash
+echo "$(date +%H:%M:%S) runners=$(ls -d /tmp/hermes_kernel_* 2>/dev/null | while read d; do [ -f "$d/pid" ] && kill -0 "$(cat $d/pid)" 2>/dev/null && echo x; done | wc -l)"
+```
+
+---
+
+## 我也会看这些
+
+压测结束后,我从外部核对:
+- 每个房间的文件是否真的写了
+- 每个文件里的 token 是否和它所属房间一致(查串台)
+- s3 链接是否都能访问
+
+---
+
+## 你怎么汇报
+
+**压测结束后**(我会在消息里说「压测结束,请汇报」),你给:
+
+```
+## 采样序列
+<60 行,每行 mem/procs/load>
+
+## 峰值
+mem 峰值 = ?    峰值时刻 = ?
+procs 峰值 = ?
+load 峰值 = ?
+
+## 前后对比
+压测开始前 mem = ?
+压测结束后 mem = ?
+差值 = ?
+
+## 异常
+有没有 OOM / 崩溃 / 重启?
+有没有 429 / 402 / 超时错误?(从 gateway.log 看)
+
+## 文件核对
+/opt/data/work/loadtest-n10/ 下有几个文件?
+每个文件的内容 = ?
+```
+
+---
+
+🔴 **现在就开跑采样循环,然后停下等我的消息。**

+ 202 - 0
docs/03-interface/指令-kangshifu-压测阶段0和1.md

@@ -0,0 +1,202 @@
+# 压力测试 · 阶段 0(1 路)+ 阶段 1(10 路并发)
+
+用户已批准分段压测。**先做阶段 0,再做阶段 1。做完停下等我指令,不要自己继续到 20 路。**
+
+---
+
+## 🔴 硬性纪律
+
+1. **做完阶段 1 就停**,把数据报给我,不要自己往 20 路推。
+2. 每个阶段都要**在开始前和结束后各记一次内存**。
+3. **不许伪造数据**。跑失败就报失败,超时就报超时。
+4. 测试文件写到 `/opt/data/work/loadtest-<阶段>-<序号>/`,便于事后清理。
+
+---
+
+# 阶段 0:1 路验证链路(约 1 分钟)
+
+在**当前这个会话**里直接做:
+
+```
+请写一个文件 /opt/data/work/loadtest-s0/msg-0.md,内容为:
+LOADTEST s0 n=0 ts=<当前时间戳>
+然后把它上传上云(按你正常的交付流程),把 s3 链接告诉我。
+```
+
+**要记录的**:
+- 端到端耗时(从收到消息到回复 s3 链接)
+- 文件是否真的出现在 `/opt/data/work/loadtest-s0/`
+- `pushed[]` 里是否有这条,带不带 `path`
+- s3 链接能否访问
+
+### 阶段 0 结束后必须给的数据
+
+```bash
+echo "=== 文件 ==="; ls -la /opt/data/work/loadtest-s0/ 2>&1
+echo "=== pushed 记录 ==="
+python3 - <<'PY'
+import json, glob
+for f in glob.glob('/opt/data/cache/deliverable-hook/*.json'):
+    d = json.load(open(f))
+    for p in (d.get('pushed') or []):
+        if 'loadtest-s0' in json.dumps(p):
+            print(f.split('/')[-1], '->', json.dumps(p, ensure_ascii=False))
+PY
+echo "=== 内存 ==="
+cat /sys/fs/cgroup/memory.current
+cat /sys/fs/cgroup/memory.max
+```
+
+**如果阶段 0 不通,立刻停下报告,不要进阶段 1。**
+
+---
+
+# 阶段 1:10 路并发
+
+## 1.1 先记基线
+
+```bash
+echo "=== 测试前内存 ==="
+cat /sys/fs/cgroup/memory.current
+cat /sys/fs/cgroup/memory.max
+echo "=== 测试前 swap ==="
+free -m 2>/dev/null | grep -i swap
+echo "=== 测试前进程数 ==="
+ps aux | wc -l
+echo "=== 测试前 load ==="
+cat /proc/loadavg
+```
+
+## 1.2 开 10 个独立会话并发
+
+🔴 **不是在同一会话里发 10 条** —— 那会撞上 per-session turn lease,测的是串行不是并发。
+
+必须**10 个独立 session**(10 个独立房间)。
+
+**怎么造 10 个会话?** 用平台云函数 `RP5Ht55NeX` 为同一个用户签 10 次身份、建 10 个房间。
+或者用你已经有的方式:**在 10 个不同的 Matrix DM 房间里各发一条消息**。
+
+**如果做不到 10 个独立会话**,如实告诉我,不要用「同一会话发 10 条」冒充。
+
+## 1.3 并发指令(10 条,内容各不相同,便于识别串台)
+
+每条消息内容模板:
+
+```
+请写一个文件 /opt/data/work/loadtest-s1/msg-<N>.md,内容为:
+LOADTEST s1 n=<N> token=<随机串> ts=<当前时间戳>
+
+写完上传上云,把 s3 链接回复给我。不要做别的事。
+```
+
+`<N>` 从 1 到 10,**同时发出**(不要间隔)。
+
+## 1.4 观测
+
+**发送后每 10 秒记一次**:
+
+```bash
+for i in $(seq 1 30); do
+  echo "$(date +%H:%M:%S) mem=$(cat /sys/fs/cgroup/memory.current) procs=$(ps aux | wc -l) load=$(cat /proc/loadavg | cut -d' ' -f1)"
+  sleep 10
+done
+```
+
+(或者你用自己的方式采样,但要能给出**时间序列**)
+
+## 1.5 结果核对(这部分最关键)
+
+```bash
+echo "=== 1. 文件产出数量(应该 10 个)==="
+ls -la /opt/data/work/loadtest-s1/ 2>&1
+
+echo "=== 2. 每个文件的内容是否对得上(查串台)==="
+for f in /opt/data/work/loadtest-s1/*.md; do
+  echo "--- $f"
+  cat "$f"
+done
+
+echo "=== 3. pushed 登记 ==="
+python3 - <<'PY'
+import json, glob
+hits = []
+for f in glob.glob('/opt/data/cache/deliverable-hook/*.json'):
+    d = json.load(open(f))
+    for p in (d.get('pushed') or []):
+        if 'loadtest-s1' in json.dumps(p):
+            hits.append((f.split('/')[-1], p.get('name'), p.get('url'), p.get('path')))
+print("total:", len(hits))
+for h in sorted(hits):
+    print(h)
+PY
+```
+
+---
+
+## 🔴 必须回答的四个问题(这是压测的核心)
+
+```
+## 1. 完整性
+10 条指令,实际完成几条?
+(缺的哪几条?为什么?超时 / 报错 / 没反应?)
+
+## 2. 串台
+有没有出现「A 会话的回复跑到了 B 会话」?
+依据:每个文件的 token 和它所属会话是否一致
+
+## 3. 资源
+测试前 mem = ?
+峰值 mem = ?(时间序列里最大的那个)
+测试后(结束后 60 秒)mem = ?
+内存有没有回落到接近基线?
+
+## 4. 失败模式
+如果有失败,失败是怎么表现的?
+- 超时没有任何响应?
+- 报错?
+- 上云失败?
+- 文件写了但没上云?
+```
+
+---
+
+# 回给我的格式
+
+```
+## 阶段 0
+端到端耗时 = ?
+文件存在 = ?
+pushed 有 path = ?
+s3 可访问 = ?
+链路 = 通 / 不通
+
+## 阶段 1
+### 1.1 基线
+mem = ? / max = ?
+swap = ?
+load = ?
+
+### 1.2 会话构造方式
+(说明你怎么造 10 个独立会话的)
+
+### 1.3 时间序列
+每 10 秒的 mem/procs/load = ?(至少 15 个点)
+
+### 1.4 完整性
+完成 = ? / 10
+
+### 1.5 串台
+有 / 无,依据 = ?
+
+### 1.6 失败明细
+<逐条列出>
+
+### 1.7 文件核对
+ls 输出 = ?
+每个文件的 token 归属 = ?
+
+## 结论
+能不能进 20 路 = ?(你的依据)
+```
+
+🔴 **做完阶段 1 就停。不要自己跑 20 路。**

+ 115 - 0
docs/03-interface/指令-kangshifu-授权采样落盘并核对.md

@@ -0,0 +1,115 @@
+# 授权:采样日志可以写 /tmp
+
+## 回答你的问题
+
+**是的,授权**。采样日志可以写 `/tmp`。这条不算「主动写文件」的禁令范围 —— 禁令针对的是**不要主动产出交付物**,采样器日志是观测工具本身。
+
+```bash
+nohup sh -c 'for i in $(seq 1 90); do
+  M=$(cat /sys/fs/cgroup/memory.current)
+  P=$(ps aux | wc -l)
+  L=$(cut -d" " -f1 /proc/loadavg)
+  R=$(ls -d /tmp/hermes_kernel_* 2>/dev/null | wc -l)
+  echo "$(date +%H:%M:%S) mem_mb=$((M/1048576)) procs=$P load=$L kernels=$R"
+  sleep 10
+done' > /tmp/loadtest-sample.log 2>&1 &
+echo "采样已启动 PID=$!"
+```
+
+**现在就启动,然后不用管它。**
+
+---
+
+## 30 路压测马上开始(我从外部发)
+
+### 这次是真测试,指令会强制走交付链路
+
+每条消息要求:
+1. 用 shell 创建 `/opt/data/work/loadtest-n30/msg-<i>.md`
+2. 内容约 **8KB**(120 行,每行带唯一 token)
+3. 回复正文里**单独一行**写 `MEDIA:/opt/data/work/loadtest-n30/msg-<i>.md`
+4. 回复里原样出现 token
+
+**第 3 条是关键** —— 让它走 `plugin-deliverable-inject` → 出站媒体 → `/deliverables/` 前缀。
+
+🔴 **如果这条链路是坏的(比如 `plugin-deliverable-inject` 没装或没生效),请如实告诉我,不要自己改走 `uploader.mjs` 绕过。**
+绕过的话链接会是 `/user/<uid>/report/...`,App 不认,测出来是假的。
+
+---
+
+## 压测结束后核对(等我说「压测结束」)
+
+```bash
+echo "=== ① 文件数量(应 30 个)==="
+ls -la /opt/data/work/loadtest-n30/ 2>&1 | head -40
+ls /opt/data/work/loadtest-n30/*.md 2>/dev/null | wc -l
+
+echo "=== ② 抽样看 3 个文件内容(验证没错乱/没串台)==="
+for i in 1 15 30; do
+  f=/opt/data/work/loadtest-n30/msg-$i.md
+  echo "--- msg-$i.md"
+  head -2 "$f" 2>&1
+  echo "   行数=$(wc -l < "$f" 2>/dev/null)  字节=$(stat -c%s "$f" 2>/dev/null)"
+done
+
+echo "=== ③ 文件里有没有别的房的 token(串台检测)==="
+python3 - <<'PY'
+import glob, re, os
+files = sorted(glob.glob('/opt/data/work/loadtest-n30/msg-*.md'))
+tokens = {}
+for f in files:
+    m = re.search(r'token=(T\d+-[A-Z0-9]+)', open(f, errors='ignore').read())
+    if m: tokens[f] = m.group(1)
+allt = set(tokens.values())
+for f, t in tokens.items():
+    txt = open(f, errors='ignore').read()
+    leak = [x for x in allt if x != t and x in txt]
+    if leak:
+        print(f"🔴 串台 {os.path.basename(f)}: {leak}")
+print(f"共 {len(files)} 文件,{len(allt)} 个不同 token,串台 {sum(1 for f,t in tokens.items() if any(x!=t and x in open(f,errors='ignore').read() for x in allt))} 个")
+PY
+
+echo "=== ④ pushed 登记(注意:这条链路可能本来就没在跑)==="
+python3 - <<'PY'
+import json, glob
+hits = 0
+for f in glob.glob('/opt/data/cache/deliverable-hook/*.json'):
+    d = json.load(open(f))
+    for p in (d.get('pushed') or []):
+        if 'loadtest-n30' in json.dumps(p): hits += 1
+print("pushed 里 loadtest-n30 条数 =", hits)
+PY
+
+echo "=== ⑤ 采样日志 ==="
+cat /tmp/loadtest-sample.log
+```
+
+---
+
+## 回给我的格式
+
+```
+## 采样
+<全部行>
+
+## 峰值 / 前后
+mem 峰值 = ? MB @ ?
+procs 峰值 = ?
+load 峰值 = ?
+kernels 峰值 = ?
+开始前 = ? MB   结束后 = ? MB   差值 = ?
+
+## 文件
+文件数 = ? / 30
+抽样内容 = ?
+串台 = ? 个
+
+## 交付链路
+回复里出现了 /deliverables/ 链接吗?出现几个?
+(没有的话:是 plugin-deliverable-inject 的问题,还是你没按要求写 MEDIA 行?如实说)
+
+## 异常
+OOM / 崩溃 / 429 / 402 / timeout?
+```
+
+🔴 **不要绕过链路。** 链路坏了就报坏,我要知道真实状态。

+ 85 - 0
docs/03-interface/指令-kangshifu-查enqueueArtifact真实调用.md

@@ -0,0 +1,85 @@
+# 查清楚:你到底在调哪个 action?
+
+## 我这边的实测结论(先告诉你,别猜了)
+
+我用用户 token 直接打了 `THXAOGKiPK`:
+
+```
+POST /api/functions {"id":"THXAOGKiPK","token":"<ksftest01>","params":{"action":"pendingArtifacts"}}
+→ {"code":400,"msg":"unknown action: pendingArtifacts"}
+```
+
+**这个云函数实际支持的 action 只有 6 个**(我从线上代码里提取的):
+
+```
+listBySession, registerFile, listByUrl, listByAgent, listByGroup, claimToGroup
+```
+
+**没有 `pendingArtifacts`,也没有 `enqueueArtifact`。**
+
+所以「pendingArtifacts 返回 jobs: []」这个描述不准确 —— 真实情况是**每次调用都被 400 拒了**。
+
+---
+
+## 我要查什么
+
+### 1. 你的代码里到底调了什么 action
+
+```bash
+echo "=== 找 enqueueArtifact / pendingArtifacts 的调用点 ==="
+grep -rn "enqueueArtifact\|pendingArtifacts" /opt/data/scripts/ /opt/data/plugins/ /opt/hermes/ 2>/dev/null | head -30
+
+echo "=== 找 _hub_call / callFunction 的实现 ==="
+grep -rn "def _hub_call\|def _call_hub\|THXAOGKiPK" /opt/data/scripts/ /opt/data/plugins/ 2>/dev/null | head -20
+```
+
+### 2. 贴出实际发送的请求体
+
+找到调用点后,把它**构造的 params 原文**贴出来。我要看 `action` 字段到底是什么。
+
+### 3. 从日志里捞真实的 HTTP 响应
+
+```bash
+echo "=== agent.log 里的 400 / unknown action ==="
+grep -n "unknown action\|enqueueArtifact\|pendingArtifacts" /opt/data/logs/agent.log 2>/dev/null | tail -20
+
+echo "=== 完整的失败调用上下文 ==="
+grep -n -B3 -A6 "unknown action" /opt/data/logs/agent.log 2>/dev/null | tail -40
+```
+
+### 4. 你说「08:33 还正常、10:11 突然坏」——查这个转折
+
+```bash
+echo "=== 08:33 附近成功登记的那次,走的是什么 ==="
+grep -n "CWg5NrnA6C\|08:33" /opt/data/logs/agent.log 2>/dev/null | head -10
+
+echo "=== 08:00-10:30 之间,脚本/插件有没有被改过 ==="
+find /opt/data/scripts /opt/data/plugins -newermt "2026-11-14 08:00" ! -newermt "2026-11-14 10:30" -type f -printf '%TH:%TM %p\n' 2>/dev/null | sort | head -30
+```
+
+---
+
+## 回答格式
+
+```
+## 1. 调用点
+<grep 结果>
+
+## 2. 实际 params
+```python
+<构造请求的那段代码原文>
+```
+action 字段的实际值 = ?
+
+## 3. 日志里的真实响应
+<原文>
+
+## 4. 08:33 → 10:11 的转折
+08:33 那次调的是哪个 action?
+这期间文件有被改过吗?
+
+## 5. 你的判断
+为什么一个「不存在的 action」在 08:33 能用、10:11 不能用?
+```
+
+🔴 **只要事实和代码原文。不要提议修复方案,先查清楚。**

+ 109 - 0
docs/03-interface/问-kangshifu-压测前勘察.md

@@ -0,0 +1,109 @@
+# 压力测试前:只读勘察(不要做任何改动)
+
+我准备对这台容器做并发压力测试(10 / 20 / 30 路同时开多个会话,每路让它**写文件并上云**)。
+
+**先做只读勘察,把能力边界和当前负载摸清楚。全部只读,不要改任何东西,不要跑任何测试。**
+
+---
+
+## 1. 当前是否有任务在跑
+
+```bash
+echo "=== currentSessions(容器自报)==="
+python3 - <<'PY'
+import json, os
+for p in ['/opt/data/agent_state.json', '/opt/data/state/agent_state.json']:
+    if os.path.isfile(p):
+        d = json.load(open(p))
+        print(p, '->', json.dumps(d.get('extra', {}).get('currentSessions', d.get('currentSessions')), ensure_ascii=False))
+        break
+else:
+    print("(没找到 agent_state.json)")
+PY
+
+echo "=== 活着的 hermes 任务进程 ==="
+ps -eo pid,etimes,cmd | grep -iE "hermes|gateway" | grep -v grep | head -20
+
+echo "=== 最近 10 分钟有没有新消息 ==="
+ls -lt --time-style=+%H:%M /opt/data/cache/deliverable-hook/*.json 2>/dev/null | head -5
+```
+
+## 2. 并发能力上限(读源码,不要猜)
+
+```bash
+echo "=== 模型 provider 并发/限流配置 ==="
+grep -rn "max_concurrent\|concurrency\|semaphore\|max_workers\|rate_limit\|parallel" /opt/data/config.yaml 2>/dev/null
+echo "--- 完整 config.yaml 里与并发相关的段 ---"
+grep -n -B2 -A6 "concurren\|parallel\|worker\|limit" /opt/data/config.yaml 2>/dev/null | head -60
+
+echo "=== agent/session 相关配置 ==="
+grep -n -A15 "^agent:\|^session:\|^matrix:" /opt/data/config.yaml 2>/dev/null | head -60
+```
+
+**要回答**:
+- 有没有并发上限配置?数值是多少?
+- 有没有每会话的队列/超时?
+
+## 3. 容器资源余量
+
+```bash
+echo "=== CPU ==="
+nproc
+echo "=== 内存 ==="
+free -h 2>/dev/null || cat /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.max 2>/dev/null
+echo "=== 磁盘 ==="
+df -h /opt/data
+echo "=== 当前 Python 进程数与内存 ==="
+ps -eo pid,rss,cmd --sort=-rss | grep -i python | grep -v grep | head -10
+```
+
+## 4. 上一轮遗留的测试目录
+
+```bash
+echo "=== /opt/data/work 现状 ==="
+ls -la --time-style=+%m-%d_%H:%M /opt/data/work/
+du -sh /opt/data/work/* 2>/dev/null | sort -rh | head -10
+```
+
+## 5. 文件产出的现有链路是否正常(单次验证)
+
+**只发一条最简单的**,确认链路没坏:
+
+```bash
+# 只写文件,不上传、不发消息
+echo "smoke $(date -Is)" > /opt/data/work/_smoke_test.txt
+ls -la /opt/data/work/_smoke_test.txt
+rm -f /opt/data/work/_smoke_test.txt
+echo "写删正常"
+```
+
+---
+
+# 回答格式
+
+```
+## 1. 当前负载
+currentSessions = ?
+活进程数 = ?
+最近活动时间 = ?
+
+## 2. 并发上限
+配置里的并发相关项 = ?
+有没有硬上限 = ? 数值 = ?
+
+## 3. 资源
+CPU 核数 = ?
+内存 已用/上限 = ?
+磁盘 已用/总量 = ?
+
+## 4. 遗留
+/opt/data/work 下有哪些目录 = ?
+
+## 5. 链路
+写删测试 = ?
+
+## 6. 你判断能扛多少路并发
+(只给判断依据,不要建议我怎么测)
+```
+
+🔴 **只读。不要开始任何压力测试。不要修改任何配置。**

+ 100 - 0
docs/03-interface/问-ljtest-文件交付五步流程.md

@@ -0,0 +1,100 @@
+# 请把「容器产文件 → 上传 → 台账 → App 展示」这条链路的**真实代码**贴出来
+
+## 背景
+
+另一台容器 `agent-node-kangshifu` 的交付链路坏了。它自己直传 S3,拿到的是
+`https://s3.fmode.cn/user/<uid>/report/...`,前缀不对,App 不认。
+
+你说过:
+
+> **key = `deliverables/<agentId>/<YYYYMM>/<deliverableId>/<token>-<filename>` 完全由云函数 `lfYlgU7SkK` 服务端决定,容器侧零参与。**
+
+我要知道**完整的那条路怎么走的**,每一步的真实代码。
+
+---
+
+## 1. 容器侧:怎么把「有个文件要交付」告诉平台
+
+```bash
+grep -rn "enqueueArtifact\|pendingArtifacts\|ackArtifact" /opt/data/scripts/ /opt/data/plugins/ 2>/dev/null
+```
+
+**贴出调用点全文**(含上下 20 行)。我要看:
+- 传了哪些参数
+- `source` 传的是什么(本地路径?)
+- 有没有 `deliverableId`,从哪来
+
+---
+
+## 2. 容器侧:怎么领任务、怎么上传
+
+```bash
+grep -rn "put_signed_url\|uploadUrl\|ackArtifact" /opt/data/scripts/ /opt/data/plugins/ 2>/dev/null | head -20
+```
+
+贴出**真正执行 PUT 的那段代码**。
+
+---
+
+## 3. 台账登记:什么时候调、传什么
+
+```bash
+grep -n "_hub_register" -A 20 /opt/data/scripts/fmode-deliverable-hook.py 2>/dev/null
+grep -rn "registerFile" /opt/data/scripts/ /opt/data/plugins/ 2>/dev/null | head
+```
+
+要回答:
+- 登记发生在**上传之前还是之后**?
+- `uploadedBy` 传什么?
+- `roomId` / `threadRoot` 怎么来的?
+
+---
+
+## 4. 关键:为什么你不需要 `AlP56LCKFm`
+
+```bash
+grep -rn "AlP56LCKFm\|uploader.mjs" /opt/data/scripts/ /opt/data/plugins/ 2>/dev/null | head
+```
+
+**确认零命中**(或者贴出命中位置)。
+
+---
+
+## 5. 最终发给 App 的 `m.file` 事件长什么样
+
+```bash
+grep -rn "m.file\|msgtype.*file\|MEDIA:" /opt/data/scripts/fmode_outbound_media.py 2>/dev/null | head -15
+```
+
+贴出**构造事件 content 的那段代码**。我要看 `url` 字段塞的是什么。
+
+---
+
+## 回答格式
+
+```
+## 1. 通知平台
+<代码原文>
+参数 = ?
+
+## 2. 领任务 + 上传
+<代码原文>
+
+## 3. 台账
+<代码原文>
+时机 = 上传前 / 后
+uploadedBy = ?
+roomId 来源 = ?
+
+## 4. 是否用 AlP56LCKFm
+<结果>
+
+## 5. m.file 事件
+<代码原文>
+url 字段 = ?
+
+## 6. 完整时序
+1. ... → 2. ... → 3. ...(用一句话一步)
+```
+
+🔴 **只要原文和事实,不要建议。**

+ 406 - 0
scripts/loadtest-agent.mjs

@@ -0,0 +1,406 @@
+#!/usr/bin/env node
+/**
+ * 容器并发压力测试(多会话)
+ *
+ * 用法:
+ *   node scripts/loadtest-agent.mjs --agent agent-node-kangshifu --n 10
+ *   node scripts/loadtest-agent.mjs --agent agent-node-kangshifu --n 10 --timeout 600
+ *
+ * ## 为什么需要它
+ *
+ * 容器自己造不出 N 个独立会话 —— 它只有自己的 agent 身份,没有用户侧身份
+ * (2026-11-14 kangshifu 实测:4 条路全不通)。
+ *
+ * 本脚本从**外部**用 `RP5Ht55NeX` 给用户签身份、建 N 个独立房间,
+ * 再**同时**发消息,制造真实并发。
+ *
+ * ## 测什么
+ *
+ *   1. 完整性:N 条里几条真的回了
+ *   2. 串台:每个房间的回复里有没有混进别的房间的 token
+ *   3. 端到端耗时:发出 → 收到 ✅ 用了多久
+ *   4. 文件产出:回复里出现的 s3 链接
+ *
+ * 🔴 **每个房间的指令带唯一 token**,回复里必须出现**自己那条**的 token。
+ *    出现别人的 token = 串台,这是「一房间一会话」的核心验证。
+ */
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { homedir } from 'node:os';
+import { join, dirname } from 'node:path';
+
+const FUNCTIONS = 'https://server.fmode.cn/api/functions';
+const FN_PROVISION = 'RP5Ht55NeX';
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+function arg(name, fallback) {
+  const i = process.argv.indexOf(`--${name}`);
+  return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
+}
+
+const AGENT = arg('agent', 'agent-node-kangshifu');
+const N = Number(arg('n', '10'));
+/** 单条等待上限(秒)。容器并发时可能排队,给足 */
+const TIMEOUT_S = Number(arg('timeout', '600'));
+/** 每轮轮询间隔(毫秒) */
+const POLL_MS = Number(arg('poll', '3000'));
+const OUT = arg('out', `docs/03-interface/loadtest-${AGENT}-n${N}.md`);
+
+function loadToken() {
+  /* 允许用 --token 或 LT_TOKEN 覆盖,方便用测试账号(如 ksftest01)跑压测 */
+  const override = arg('token', process.env.LT_TOKEN);
+  if (override) {
+    if (!override.startsWith('r:')) throw new Error('--token 必须以 r: 开头');
+    return { token: override, src: '--token/环境变量' };
+  }
+  const p = join(homedir(), '.fmode', 'config', 'user.json');
+  const t = JSON.parse(readFileSync(p, 'utf8'))?.sessionToken;
+  if (typeof t !== 'string' || !t.startsWith('r:')) throw new Error(`读不到会话凭据:${p}`);
+  return { token: t, src: p };
+}
+const { token: TOKEN, src: TOKEN_SRC } = loadToken();
+
+async function provision(agentId) {
+  const r = await fetch(FUNCTIONS, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'ncloudmaster' },
+    body: JSON.stringify({ id: FN_PROVISION, token: TOKEN, params: { agentId } }),
+  });
+  const j = await r.json();
+  const d = j.result ?? j;
+  if (!d?.accessToken) throw new Error(`provision 失败:${JSON.stringify(j).slice(0, 300)}`);
+  return d;
+}
+
+/** 生成一个可辨识的 token(8 位) */
+function mkToken(i) {
+  return `T${i}-${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
+}
+
+async function main() {
+  console.log(`\n=== 容器并发压测 ===`);
+  console.log(`容器  : ${AGENT}`);
+  console.log(`并发数: ${N}`);
+  console.log(`凭据  : ${TOKEN_SRC}`);
+  console.log(`超时  : ${TIMEOUT_S}s\n`);
+
+  const prov = await provision(AGENT);
+  const agentInfo = await provision(AGENT);
+  console.log(`用户: ${prov.userMxId} | 容器: ${agentInfo.agentMxId}\n`);
+
+  const mx = await import('matrix-js-sdk');
+  const client = mx.createClient({
+    baseUrl: prov.homeserver,
+    accessToken: prov.accessToken,
+    userId: prov.userMxId,
+  });
+  const hs = prov.homeserver.replace(/\/$/, '');
+  const auth = { Authorization: `Bearer ${prov.accessToken}` };
+
+  // ── 1. 建 N 个独立房间 ──────────────────────────────────────────────
+  console.log('=== [1] 建房间 ===');
+  const peers = [];
+  for (let i = 1; i <= N; i++) {
+    const room = (
+      await client.createRoom({
+        invite: [agentInfo.agentMxId],
+        name: `loadtest-n${N}-${i}-${Date.now()}`,
+        preset: mx.Preset.TrustedPrivateChat,
+      })
+    ).room_id;
+    peers.push({ i, room, token: mkToken(i), sentId: null, ackedAt: null, events: [], joined: false });
+    console.log(`  [${i}] ${room}`);
+  }
+
+  // ── 2. 等容器进房(并发等待,最多 120 秒)───────────────────────────
+  console.log('\n=== [2] 等容器进房 ===');
+  const joinDeadline = Date.now() + 120_000;
+  while (Date.now() < joinDeadline) {
+    const pend = peers.filter((p) => !p.joined);
+    if (!pend.length) break;
+    await Promise.all(
+      pend.map(async (p) => {
+        const r = await fetch(
+          `${hs}/_matrix/client/v3/rooms/${encodeURIComponent(p.room)}/joined_members`,
+          { headers: auth },
+        ).catch(() => null);
+        const j = r ? await r.json().catch(() => ({})) : {};
+        if ((j.joined ?? {})[agentInfo.agentMxId]) p.joined = true;
+      }),
+    );
+    await sleep(2000);
+  }
+  const joinedCount = peers.filter((p) => p.joined).length;
+  console.log(`  已进房: ${joinedCount}/${N}`);
+  if (joinedCount === 0) throw new Error('容器一个房间都没进,压测无法进行');
+
+  // ── 3. 同时发消息 ────────────────────────────────────────────────────
+  console.log('\n=== [3] 并发发送 ===');
+  const t0 = Date.now();
+  /**
+   * 🔴 指令必须逼它走**真实交付链路**(2026-11-14 修正)。
+   *
+   * 上一版的坑:只说「写文件并上传」,容器用 `uploader.mjs put` 直传,
+   * 链接是 `https://s3.fmode.cn/user/<uid>/report/...` —— **不是**
+   * `/deliverables/<agentId>/...`,App 侧 `isAgentOutputUrl()` 不认,
+   * 等于测了个假链路。
+   *
+   * 现在明确要求:写 8KB 内容(足够撑满一次真实上传)+ 回复时用 `MEDIA:` 行,
+   * 走 `plugin-deliverable-inject` → 出站媒体 → `/deliverables/` 前缀。
+   */
+  const payloadFor = (i, token) => {
+    /* 约 8KB 的可辨识内容:既压上传,又能验证文件内容没错乱 */
+    const lines = [];
+    for (let k = 0; k < 120; k++) {
+      lines.push(`L${String(k).padStart(3, '0')} n=${i} token=${token} payload=${'x'.repeat(40)}`);
+    }
+    return { head: `LOADTEST n=${i} token=${token}`, body: lines.join('\n') };
+  };
+
+  await Promise.all(
+    peers.map(async (p) => {
+      if (!p.joined) return;
+      const { head, body } = payloadFor(p.i, p.token);
+      p.expectBytes = body.length;
+      const body400 = [
+        `压测任务 ${p.i}。严格按下面做,不要做别的事,不要解释:`,
+        ``,
+        `1. 用 shell 创建文件 /opt/data/work/loadtest-n${N}/msg-${p.i}.md,内容严格为下面 ${body.split('\n').length} 行(每行都要有):`,
+        ``,
+        '```',
+        body,
+        '```',
+        ``,
+        `2. 确认第一行是:${head}`,
+        `3. 在回复正文里**单独一行**写:MEDIA:/opt/data/work/loadtest-n${N}/msg-${p.i}.md`,
+        `4. 回复里必须原样出现 token:${p.token}`,
+      ].join('\n');
+      p.promptBytes = body400.length;
+      const sent = await client.sendEvent(p.room, 'm.room.message', {
+        msgtype: 'm.text',
+        body: body400,
+        'm.mentions': { user_ids: [agentInfo.agentMxId] },
+      });
+      p.sentId = sent.event_id;
+      p.sentAt = Date.now();
+    }),
+  );
+  console.log(`  ${peers.filter((p) => p.sentId).length} 条已发出(每条含约 8KB 写入负载),等待回复…\n`);
+
+  // ── 4. 轮询收集 ──────────────────────────────────────────────────────
+  const deadline = t0 + TIMEOUT_S * 1000;
+  let lastReport = 0;
+  while (Date.now() < deadline) {
+    await sleep(POLL_MS);
+
+    await Promise.all(
+      peers.map(async (p) => {
+        if (!p.sentId || p.ackedAt) return;
+        const r = await fetch(
+          `${hs}/_matrix/client/v3/rooms/${encodeURIComponent(p.room)}/messages?dir=b&limit=60`,
+          { headers: auth },
+        ).catch(() => null);
+        if (!r) return;
+        const j = await r.json().catch(() => ({}));
+        const seen = new Set(p.events.map((e) => e.id));
+        for (const ev of (j.chunk ?? []).reverse()) {
+          if (seen.has(ev.event_id)) continue;
+          if (ev.type === 'm.reaction') {
+            const rel = ev.content?.['m.relates_to'];
+            if (rel?.event_id === p.sentId && String(rel.key ?? '').includes('✅')) {
+              p.ackedAt = Date.now();
+            }
+            p.events.push({ id: ev.event_id, type: 'reaction', body: '' });
+            continue;
+          }
+          if (ev.type !== 'm.room.message' || ev.sender === prov.userMxId) continue;
+          p.events.push({
+            id: ev.event_id,
+            type: ev.content?.msgtype || 'm.text',
+            body: String(ev.content?.body ?? ''),
+            file: ev.content?.file,
+          });
+        }
+      }),
+    );
+
+    const done = peers.filter((p) => p.ackedAt).length;
+    if (Date.now() - lastReport > 15000) {
+      lastReport = Date.now();
+      const elapsed = Math.round((Date.now() - t0) / 1000);
+      console.log(`  [${elapsed}s] 完成 ${done}/${peers.filter((p) => p.joined).length}`);
+    }
+    if (done === peers.filter((p) => p.joined).length) break;
+  }
+
+  // ── 5. 结果分析 ──────────────────────────────────────────────────────
+  const bound = peers.filter((p) => p.joined);
+  const rows = bound.map((p) => {
+    const bodies = p.events.filter((e) => e.body).map((e) => e.body).join('\n');
+    /* 串台检测:这个房间的回复里出现了**别的房间**的 token */
+    const others = bound.filter((q) => q !== p).map((q) => q.token);
+    const leakedIn = others.filter((t) => bodies.includes(t));
+    const hasOwn = bodies.includes(p.token);
+    const urls = [...bodies.matchAll(/https:\/\/s3\.fmode\.cn\/[^\s)`]+/g)].map((m) => m[0]);
+    const files = p.events.filter((e) => e.file).map((e) => e.file?.url).filter(Boolean);
+    const allUrls = [...new Set([...urls, ...files])];
+    /*
+     * 🔴 链接前缀判定(2026-11-14 新增)。
+     *
+     * App 侧 `s3-link.ts:290` 要求 `/deliverables/` 前缀,否则不认成果。
+     * 上一版压测容器走 `uploader.mjs put`,给出 `/user/<uid>/report/...`,
+     * 测出来"成功"但 App 根本看不到 —— 这是假测试。
+     */
+    const deliverableUrls = allUrls.filter((u) => u.includes('/deliverables/'));
+    const wrongPrefixUrls = allUrls.filter((u) => !u.includes('/deliverables/'));
+    return {
+      i: p.i,
+      room: p.room,
+      token: p.token,
+      ok: !!p.ackedAt,
+      ms: p.ackedAt ? p.ackedAt - p.sentAt : null,
+      hasOwn,
+      leakedIn,
+      urls: allUrls,
+      deliverableUrls,
+      wrongPrefixUrls,
+      replyLen: bodies.length,
+      expectBytes: p.expectBytes || 0,
+    };
+  });
+
+  // ── 5b. S3 三重验证(并发 HEAD,最多 30 秒)─────────────────────────
+  console.log('\n=== [4b] 验证 s3 链接 ──');
+  await Promise.all(
+    rows.map(async (r) => {
+      r.urlChecks = await Promise.all(
+        r.urls.map(async (u) => {
+          try {
+            const ac = new AbortController();
+            const to = setTimeout(() => ac.abort(), 15000);
+            const resp = await fetch(u, { method: 'GET', signal: ac.signal });
+            clearTimeout(to);
+            const text = resp.ok ? await resp.text() : '';
+            return {
+              url: u,
+              status: resp.status,
+              ok: resp.ok,
+              bytes: text.length,
+              /* 内容里有没有本房的 token —— 验证「文件内容没错乱/没串台」 */
+              hasOwnToken: text.includes(r.token),
+              hasOtherToken: rows.some((q) => q.i !== r.i && text.includes(q.token)),
+            };
+          } catch (e) {
+            return { url: u, status: 0, ok: false, error: String(e?.message || e) };
+          }
+        }),
+      );
+      r.contentOk = r.urlChecks.some((c) => c.ok && c.hasOwnToken && !c.hasOtherToken);
+      r.sizeExpectOk = r.urlChecks.some((c) => c.ok && c.bytes >= r.expectBytes * 0.9);
+    }),
+  );
+
+  const doneN = rows.filter((r) => r.ok).length;
+  const crossTalk = rows.filter((r) => r.leakedIn.length > 0);
+  const noOwn = rows.filter((r) => r.ok && !r.hasOwn);
+  const withDeliverable = rows.filter((r) => r.deliverableUrls.length > 0);
+  const wrongPrefix = rows.filter((r) => r.urls.length > 0 && r.deliverableUrls.length === 0);
+  const contentVerified = rows.filter((r) => r.contentOk);
+
+  console.log('\n=== [5] 结果 ===');
+  console.log(`完整性     : ${doneN}/${bound.length}`);
+  console.log(`串台(消息): ${crossTalk.length} 个房间收到别人的 token`);
+  console.log(`有回复但无本房 token : ${noOwn.length}`);
+  console.log(`🔗 /deliverables/ 前缀 : ${withDeliverable.length}/${rows.filter((r) => r.urls.length).length}`);
+  console.log(`⚠️ 链接前缀错误 : ${wrongPrefix.length}`);
+  console.log(`📄 内容验证通过 : ${contentVerified.length}/${rows.filter((r) => r.urls.length).length}`);
+  const times = rows.filter((r) => r.ms).map((r) => r.ms);
+  if (times.length) {
+    times.sort((a, b) => a - b);
+    const p = (q) => times[Math.min(times.length - 1, Math.floor(times.length * q))];
+    console.log(`耗时(秒)   : min=${(times[0] / 1000).toFixed(1)} p50=${(p(0.5) / 1000).toFixed(1)} p95=${(p(0.95) / 1000).toFixed(1)} max=${(times[times.length - 1] / 1000).toFixed(1)}`);
+  }
+
+  for (const r of rows) {
+    const chk = r.urlChecks?.[0];
+    const flags = [
+      r.ok ? `✅ ${(r.ms / 1000).toFixed(1)}s` : '❌ 超时无 ✅',
+      r.hasOwn ? '' : '⚠️回复无本房token',
+      r.leakedIn.length ? `🔴串台:${r.leakedIn.join(',')}` : '',
+      r.deliverableUrls.length ? `🔗deliverables` : r.urls.length ? '⚠️前缀非deliverables' : '❌无链接',
+      chk ? (chk.ok ? `S3 ${chk.status}/${chk.bytes}B` : `S3 ${chk.status}`) : 'S3 未测',
+      r.contentOk ? '内容✓' : r.urls.length ? '内容✗' : '',
+    ]
+      .filter(Boolean)
+      .join(' ');
+    console.log(`  [${String(r.i).padStart(2)}] ${flags}`);
+  }
+
+  // ── 6. 落盘 ──────────────────────────────────────────────────────────
+  const lines = [
+    `# 压测结果 — ${AGENT} — 并发 ${N}`,
+    ``,
+    `- 时间:${new Date().toISOString()}`,
+    `- 用户:${prov.userMxId}`,
+    `- 容器:${agentInfo.agentMxId}`,
+    `- 超时:${TIMEOUT_S}s`,
+    ``,
+    `## 汇总`,
+    ``,
+    `| 指标 | 值 |`,
+    `|---|---|`,
+    `| 建房间 | ${N} |`,
+    `| 容器进房 | ${bound.length} |`,
+    `| 发出 | ${peers.filter((p) => p.sentId).length} |`,
+    `| **完成** | **${doneN}** |`,
+    `| 串台房间数 | ${crossTalk.length} |`,
+    `| 有回复但无本房 token | ${noOwn.length} |`,
+    `| **/deliverables/ 前缀正确** | **${withDeliverable.length}** |`,
+    `| 链接前缀错误(App 不认) | ${wrongPrefix.length} |`,
+    `| **S3 内容验证通过** | **${contentVerified.length}** |`,
+    times.length ? `| 耗时 p50 | ${(times[Math.floor(times.length * 0.5)] / 1000).toFixed(1)}s |` : '',
+    times.length ? `| 耗时 max | ${(times[times.length - 1] / 1000).toFixed(1)}s |` : '',
+    ``,
+    `## 逐房间`,
+    ``,
+    `| # | 房间 | token | 完成 | 耗时 | 本房token | 串台 | deliverables | S3 | 内容 |`,
+    `|---|---|---|---|---|---|---|---|---|---|`,
+    ...rows.map(
+      (r) =>
+        `| ${r.i} | \`${r.room}\` | ${r.token} | ${r.ok ? '✅' : '❌'} | ${r.ms ? (r.ms / 1000).toFixed(1) + 's' : '—'} | ${r.hasOwn ? '✅' : '❌'} | ${r.leakedIn.length ? '🔴 ' + r.leakedIn.join(',') : '—'} | ${r.deliverableUrls.length ? '✅' : r.urls.length ? '⚠️' : '❌'} | ${r.urlChecks?.[0]?.ok ? `${r.urlChecks[0].status}/${r.urlChecks[0].bytes}B` : '—'} | ${r.contentOk ? '✅' : '❌'} |`,
+    ),
+    ``,
+    `## 串台详情`,
+    ``,
+    crossTalk.length
+      ? crossTalk.map((r) => `- 房间 ${r.i} 收到了:${r.leakedIn.join(', ')}`).join('\n')
+      : '无串台。',
+    ``,
+    `## 链接前缀问题`,
+    ``,
+    wrongPrefix.length
+      ? wrongPrefix
+          .map(
+            (r) => `- 房间 ${r.i}:\n${r.urls.map((u) => `  - \`${u}\``).join('\n')}\n  (不是 \`/deliverables/\`,App 侧 \`isAgentOutputUrl()\` 不认)`,
+          )
+          .join('\n')
+      : '全部链接都是 `/deliverables/` 前缀。',
+    ``,
+    `## 失败明细`,
+    ``,
+    rows.filter((r) => !r.ok).length
+      ? rows.filter((r) => !r.ok).map((r) => `- 房间 ${r.i}(${r.room}):${r.replyLen} 字符回复,未收到 ✅`).join('\n')
+      : '无失败。',
+    ``,
+  ];
+  mkdirSync(dirname(OUT), { recursive: true });
+  writeFileSync(OUT, lines.join('\n'));
+  console.log(`\n结果已写入 ${OUT}`);
+
+  process.exit(doneN === bound.length && crossTalk.length === 0 ? 0 : 1);
+}
+
+main().catch((e) => {
+  console.error('\n❌ 压测失败:', e instanceof Error ? e.message : e);
+  process.exit(2);
+});