# S0 — 触发率实测报告与阈值决策

> 数据来源：`C:\Users\Horus\.dsh\sessions` 下 **72 个** `session.v*.jsonl.zstd`
> 样本量：24,178 条记录，其中 **4,653 条 `tool/result`**，252 个 `turn/start`
> 复现：`node scripts/trigger-rate.ts --json trigger-rate.json`

这份文件是**所有默认值的依据**。下面的数字是 2026-09-20 那次测量的结果，没有被后续改动取代——
后续新增的能力（注入筛查、试运行、完成度闸门）都在 §8 里补了它们各自的默认值依据。

## 为什么先做这一步

`tools/post-execute` 是 **await 型 waterfall**，每次工具调用都会经过。所以决定成败的不是单次延迟，而是：

```
触发率 × 单次延迟 = 每 turn 增量
```

这个数字必须实测，不能猜。

## 1. 工具结果体积分布（估算 token）

`mean 733 · p50 125 · p90 1610 · p99 12500 · max 14800`

| 阈值 | 条数 | 占比 |
|---|---|---|
| > 1,000 | 748 | 16.1% |
| > 1,500 | 509 | 10.9% |
| > 2,000 | 377 | 8.1% |
| > 3,000 | 251 | 5.4% |
| > 6,000 | 129 | 2.8% |

**中位数只有 125 token** —— 绝大多数工具结果是小的，长尾很少但极大。这决定了阈值必须卡在 p90 以上。

## 2. 只读族占比（决定白名单）

超 1,500 token 且非错误的 509 条中，只读族占 **72.1%**：

```
web_fetch 191 · read 113 · pwsh 113 · grep 52 · cordis_inspect_query 19 · glob 11 · 其他 10
```

**`web_fetch` 是最大来源（37.5%）**——抓来的网页天然巨大。

**`pwsh` 113 条被刻意排除。** 终端输出常含构建日志、错误栈、文件列表，其中的"无关"内容往往是诊断所需；让廉价判定去裁它有实际风险。同理排除 `cordis_inspect_query`（API 转储，结构强、不该由语义判定裁剪）。

**同一份分布也是注入筛查把 `web_fetch` 放在第一位的理由**——它既是最大的来源，也是唯一真正不可信的输入。

## 3. 每 turn 触发数（决定 per-turn 配额）

252 个 turn 中 **79 个（31%）** 会产生至少一次触发。

| minTokens | 触发 turn | 均值/turn | p90 | **max** | cap3→均值 | cap3→最坏 |
|---|---|---|---|---|---|---|
| 1000 | 88 | 5.8 | 16 | **38** | 0.66s | 0.9s |
| 1500 | 79 | 4.6 | 10 | **27** | 0.63s | 0.9s |
| **2000** | **69** | **4.1** | **12** | **21** | **0.60s** | **0.9s** |
| 3000 | 53 | 3.6 | 12 | 18 | 0.54s | 0.9s |
| 6000 | 32 | 3.1 | 10 | 12 | 0.52s | 0.9s |

**两个决定性结论：**

1. **不设配额会在最坏情况下加 8.1 秒**（27 次 × 300ms）。**配额是必需品，不是优化。**
2. **加上 cap=3 之后，延迟在所有阈值下几乎持平（0.52–0.66s）。** 也就是说：**延迟由配额决定，不由阈值决定。** 因此阈值不该按延迟来选。

cap=3 时仍有 34.2% 的触发 turn 被截断（cap1 64.6%、cap2 46.8%、cap5 20.3%）——配额确实在起作用。

**配额是共享的。** 注入筛查搭车在同一次请求里时不额外消耗配额；只有"没有剪枝请求可以搭车"时它才单独消耗一次。这意味着筛查不会悄悄把剪枝的预算吃光——两者竞争的是同一个上限，而那个上限正是上面测出来的那个数。

## 4. 预估上下文节省（**模型，非实测**）

假设一次判定能丢弃 65% 的内容（受 `minKeepRatio` 0.2 下限约束）。全部工具结果合计 **3,409,687** token。

| minTokens | 判定次数 | 判定覆盖 token | 节省 token | **占全部** | 每触发 turn 节省 |
|---|---|---|---|---|---|
| 1000 | 508 | 1,991,227 | 1,294,297 | 38.0% | 14,708 |
| 1500 | 367 | 1,817,443 | 1,181,338 | 34.6% | 14,954 |
| **2000** | **284** | **1,674,735** | **1,088,578** | **31.9%** | **15,776** |
| 3000 | 191 | 1,443,868 | 938,514 | 27.5% | 17,708 |
| 6000 | 100 | 1,067,280 | 693,732 | 20.3% | 21,679 |

**这张表是模型，不是实测。** 实测只证明过管线跑通，见 §7 的现场记录；真实节省必须以台账为准（§5）。

## 5. 一个必须写清的警告：基线不是零

DSH **已经有两套削减机制**，上面 31.9% 是**毛机会，不是净增量**：

| 已有机制 | 作用方式 | 实测频率 |
|---|---|---|
| `toolResultPruner` | 对当前 surface 节点做**确定性** head/middle/tail 截断 | 不进日志，无法从历史估算 |
| `compaction` | **整条消息**遮蔽（`compaction/prune` 带 `shadowedTokenCount`，实测 2305/4818/3314） | 72 个 session 里**只有 3 次** |

**Jev 的真实增量在哪：** 确定性截断只能砍**连续**的中段；Jev 能砍**散布**的无关片段、同时保留散布的相关片段。这是真差异，但**无法从日志量化**——`toolResultPruner` 不写日志。

**因此 v0.1 内建了 A/B**：`tools/post-execute` 里直接调用 `toolResultPruner.pruneContent(blocks)` 拿到"确定性基线会保留什么"，把**基线与 Jev 的保留量一起写进台账**。这样净增量从上线第一天就可测，而不是靠事后争论。台账现已持久化，`npm run measure -- --ledger` 读的就是它。

## 6. turn 文件改动频率（当时的 v0.3 输入，现已决策）

**252 个 turn 中只有 11 个（4.4%）** 有 `workspace/changes`。

这条数字**改变了计划**，值得完整记下来：

- 立项文档里的 M2 是"在 `agent/turn-stopping` 上做完成度自检真"，并且留了一个未核实项（`agent.steer` 的确切入口）。
- 4.4% 说明**自动拦截型**的自检几乎没有可帮的人群，而它的风险（把本来正确的 turn 反复拉长）却是实的。
- 所以它没有做成钩子，而是做成了工具 **`jev_gate`**：agent 主动调用，用真实证据核对完成声明。工具形态不需要无人值守的 steer，也就没有自检循环风险，却拿到了大部分价值。
- **结论：这条路的性价比问题不是被推迟回答的，而是被换了个形态回答的。**

## 7. skill 目录规模（决定 minCatalogSize）

n=55 个投递给模型的目录：**min 27 · p50 29 · p90 31 · max 40**。

**100% 的目录 ≥ 20 个 skill。** 技能推荐不是伪需求——典型用户面对的是 **~29 个 skill 的目录**。

### 现场记录（真实会话，非实验室）

在真实会话里被观测到的四次剪枝：

```
read: 4613 → 2624 tokens（保留 8/13 段）
grep: 2932 → 1679 tokens（保留 9/15 段）
read: 2035 → 1651 tokens（保留 6/7 段）
grep: 2392 → 1126 tokens（保留 40/83 段）
```

这四次说明了三件事：管线在真实数据上跑得通；确定性首尾保底确实在起作用（上面 8/13、9/15 都是首尾留下、中段被丢）；以及**保留比例波动很大**（46%–80%），这正是"只排序、不卡阈值"的原因——没有哪个固定比例是安全的。

## 8. 最终阈值决策

| 配置项 | 决策值 | 依据 |
|---|---|---|
| `prune.minTokens` | **2000** | 延迟已被配额支配而与阈值无关；2000 保留 31.9% 的节省（vs 1500 的 34.6%，仅差 2.7pp），却少发 83 次判定——**每次判定都是一次内容出境和一次裁错风险** |
| `prune.perTurnLimit` | **3** | 无配额最坏 8.1s；cap=3 最坏 0.9s、均值 0.60s |
| `prune.toolAllowlist` | `read` `grep` `glob` `web_fetch` `web_search` | 覆盖 72–76% 的超大结果；**刻意排除 `pwsh`** |
| `prune.headLines` / `tailLines` | 40 / 40 | 确定性保底，不可协商 |
| `prune.keepHigh` | 0.5 | 高概率段无条件保留 |
| `prune.minKeepRatio` | 0.2 | 防止把结果削成空壳 |
| `prune.minSaving` | 0.15 | 削减不足则放弃，不引入失真 |
| `prune.minTaskChars` | 12 | 拿「继续吧」这类极短消息判相关性得到的是噪声 |
| `prune.shadow` | false | 默认关。它是一个**信任前的观察工具**，不是默认工作模式：开着的代价是判定照付、相同 payload 会被重复判定（缓存里存的是已精简内容，试运行不能取用） |
| `screen.enabled` | true | 默认开。抓取内容是唯一真正不可信的输入，而这一次判定的成本几乎为零（搭车同一次请求） |
| `screen.minTokens` | **300** | 与剪枝阈值**刻意不同**：最危险的页面往往是短的，短到永远进不了剪枝。300 token 足以承载一段注入指令 |
| `screen.threshold` | 0.75 | 采纳生态里通行的注入阈值起点（jev-mcp 的 `block_at` 同为 0.75）。官方明确阈值应随后果分档，并且**必须用自己的数据调** |
| `screen.toolAllowlist` | `web_fetch` `web_search` | 只查外部抓取。本地文件是用户自己的材料，且读取频繁（§2 里 `read` 113 条 vs `web_fetch` 191 条）——把它们也查会吃掉剪枝的配额 |
| `suggest.minCatalogSize` | **15** | 实测 min 27、p50 29；真实用户必触发，极简 preset 保持沉默 |
| `suggest.minConfidence` | 0.3 | 低置信度**不注入**——宁可沉默，不要噪音。注意它卡的是 Choice 的 `confidence`（分布形状派生的统计量），与 `prune.keepHigh` 卡的 **Noul 值**不是同一种量，两个数字不可比较 |
| `sessionCallLimit` | 200 | 成本与内容出境的硬上限，所有能力**共享** |

## 9. 复现与后续

- 脚本：`scripts/trigger-rate.ts`（`node scripts/trigger-rate.ts --json trigger-rate.json`）
- 会话日志格式：不是单一 zstd 流，也**不是一帧一记录**——写入方追加独立 zstd 帧，每帧含多条 JSONL 记录。Node 的 zstd API 遇多帧只解第一帧，因此必须**扫描帧魔数 + 按 `seq` 并集去重**。实现见 `scripts/lib/session-log.ts`。
- token 数为估算（`cjkChars + otherChars / 4`，约 ±20%），只用于比例与形状，不用于计费。
- 真实节省必须以台账为准（见 §5 的 A/B 要求）。台账现已持久化到 `$DSH_HOME/storages/dsh_jev_tools/`，`npm run measure -- --ledger` 可直接读它并报告相对基线的净增量。
- **仍未测的**：与主模型在同一批样本上的对照（立项文档 M0 的验收标准），以及标定曲线的真实拟合——后者需要每条判定的概率与事后对错，两者 v0.1 都没有。
