@basein/runner 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/bin/bir-hooks.d.ts +6 -5
- package/dist/bin/bir-hooks.js +7 -6
- package/dist/bin/bir.js +4 -4
- package/dist/control/server.js +13 -0
- package/dist/replay/controller.d.ts +1 -1
- package/dist/replay/controller.js +1 -1
- package/dist/replay/tool-error.d.ts +10 -9
- package/dist/replay/tool-error.js +13 -21
- package/docs/calculatedReplay.md +3 -3
- package/docs/calculatedReplayGuide.md +5 -5
- package/docs/mcpmark.md +752 -752
- package/docs/my-first-sample.md +2 -2
- package/package.json +1 -1
package/docs/mcpmark.md
CHANGED
|
@@ -1,752 +1,752 @@
|
|
|
1
|
-
# MCPMark — running one task against BaseInstRunner
|
|
2
|
-
|
|
3
|
-
A concrete plan for one task, end to end:
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
tasks/postgres/easy/employees/hiring_year_summary/
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Companion to [t-bench.md](t-bench.md). That document explains why τ²-bench was hard to
|
|
10
|
-
integrate. This one exists because **MCPMark is structurally easier and scores our product
|
|
11
|
-
more honestly**, and §1 is the reason.
|
|
12
|
-
|
|
13
|
-
Source: [github.com/eval-sys/mcpmark](https://github.com/eval-sys/mcpmark) ·
|
|
14
|
-
[paper](https://huggingface.co/papers/2509.24002) · [mcpmark.ai](https://mcpmark.ai/)
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## 1. Why this benchmark, and why this task
|
|
19
|
-
|
|
20
|
-
Three structural facts, each of which τ²-bench got wrong for us.
|
|
21
|
-
|
|
22
|
-
**MCPMark grades the world, not the transcript.** `verify.py` opens a psycopg2 connection
|
|
23
|
-
and queries the actual database. τ²-bench's `evaluator_env.py` builds a *fresh* environment
|
|
24
|
-
and replays the trajectory's `(tool_call, tool_result)` pairs onto it — which is why direct
|
|
25
|
-
mode was structurally penalised there (t-bench.md §3.1). Here, direct mode executes real
|
|
26
|
-
SQL through a real connection, the table really exists, and `verify.py` passes. **Direct
|
|
27
|
-
mode is fully scoreable on MCPMark.** This is the single most important line in this
|
|
28
|
-
document.
|
|
29
|
-
|
|
30
|
-
**The agent already speaks MCP.** MCPMark spawns `postgres-mcp` over stdio
|
|
31
|
-
(`src/agents/mcpmark_agent.py:1183`). τ²-bench has no MCP interface at all, which is why it
|
|
32
|
-
needed `_shadow_tools`, `mcp-stdio-bridge.mjs` and two copies of the domain. Here
|
|
33
|
-
`bir-proxy` just wraps the server MCPMark was going to spawn anyway. **No bridge.**
|
|
34
|
-
|
|
35
|
-
**Every task runs four times against a reset state.** That is our replay shape handed over
|
|
36
|
-
for free: trial 1 records, trials 2–4 replay. And the metric MCPMark leads with, `pass^4`,
|
|
37
|
-
collapses for every model (best is 52.56% pass@1 → 33.86% pass^4) purely from run-to-run
|
|
38
|
-
inconsistency — which is exactly what a deterministic replay removes.
|
|
39
|
-
|
|
40
|
-
### Why `hiring_year_summary` specifically
|
|
41
|
-
|
|
42
|
-
| | |
|
|
43
|
-
|---|---|
|
|
44
|
-
| Service | `postgres` — pure MCP, no built-in substitute the model would prefer |
|
|
45
|
-
| Difficulty | `L1` / `easy` bucket |
|
|
46
|
-
| Template DB | `employees` |
|
|
47
|
-
| Writes | one `CREATE TABLE` + populate — enough to be a real CRUD test, small enough to debug |
|
|
48
|
-
| Verification | ground truth computed in SQL at verify time, compared row-by-row with 0.1 tolerance on decimals |
|
|
49
|
-
| Prompt | 4 short sections, ~200 words |
|
|
50
|
-
|
|
51
|
-
It is the smallest task in the repo that still exercises write operations and per-step
|
|
52
|
-
parameter threading. Start here, then widen.
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 2. The prompt
|
|
57
|
-
|
|
58
|
-
The user prompt is `description.md` **verbatim**, plus one fixed suffix appended by
|
|
59
|
-
`BaseTaskManager._format_task_instruction`:
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
<description.md>
|
|
63
|
-
|
|
64
|
-
Note: Based on your understanding, solve the task all at once by yourself,
|
|
65
|
-
don't ask for my opinions on anything.
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Plus MCPMark's system prompt (`src/agents/mcpmark_agent.py:48`, `MAX_TURNS = 100`):
|
|
69
|
-
|
|
70
|
-
> You are a helpful agent that uses tools iteratively to complete the user's task, and when
|
|
71
|
-
> finished, provides the final answer or simply states "Task completed" without further
|
|
72
|
-
> tool calls.
|
|
73
|
-
|
|
74
|
-
**No templating, no randomisation, no timestamps.** All four trials send byte-identical
|
|
75
|
-
text. Our similarity gate sees 1.0 against a 0.92 threshold, so replay arms on trials 2–4
|
|
76
|
-
every time. Nothing in this plan depends on embedding luck.
|
|
77
|
-
|
|
78
|
-
Corollary: `ANTHROPIC_API_KEY` is **not needed**. `doctor` reporting
|
|
79
|
-
`derive=recorded sample values` is correct behaviour here, not a degradation — for an
|
|
80
|
-
identical prompt the recorded values *are* the right ones.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## 3. Topology
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
claude -p (cwd = the run dir)
|
|
88
|
-
├── mcp__bir__run_scenario ─────────────▶ bir-scenario ──┐ direct-mode delivery
|
|
89
|
-
│ │ (§4.4 — needs a trusted
|
|
90
|
-
└── mcp__postgres__* ──▶ bir-proxy ──▶ postgres-mcp │ workspace, or it is denied)
|
|
91
|
-
│ (uvx, │
|
|
92
|
-
reports every call mcp<2) │
|
|
93
|
-
▼ │ ▼
|
|
94
|
-
bir-hooks :53411 │ POST /scenario/run
|
|
95
|
-
│ │ │
|
|
96
|
-
▼ ▼ │
|
|
97
|
-
BaseIn service :8080 PostgreSQL 17 ◀┘
|
|
98
|
-
(native, :5432)
|
|
99
|
-
▲
|
|
100
|
-
verify.py ────┘
|
|
101
|
-
(grades the real database)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
The proxy holds the open connection, and that is what makes direct mode free: on
|
|
105
|
-
`/scenario/run` all eight steps execute over it for zero model tokens. `bir-scenario` is
|
|
106
|
-
how the results get back into the transcript as a genuine `tool_result` rather than an
|
|
107
|
-
injected denial — which is why §4.4 matters as much as it does.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## 4. Setup
|
|
112
|
-
|
|
113
|
-
### 4.1 Postgres, seeded
|
|
114
|
-
|
|
115
|
-
MCPMark runs `pgvector/pgvector:0.8.0-pg17-bookworm` in Docker. **This machine has no
|
|
116
|
-
Docker, no Docker Desktop and no WSL**, so we install PostgreSQL 17 natively instead —
|
|
117
|
-
the same major version. This task is plain SQL, so the missing `pgvector` extension is
|
|
118
|
-
irrelevant; note the deviation in any published result.
|
|
119
|
-
|
|
120
|
-
```powershell
|
|
121
|
-
winget install --id PostgreSQL.PostgreSQL.17 -e `
|
|
122
|
-
--accept-package-agreements --accept-source-agreements `
|
|
123
|
-
--custom "--mode unattended --superpassword password --serverport 5432 --unattendedmodeui none"
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Needs one UAC approval. The superuser password is set to `password` to match MCPMark's own
|
|
127
|
-
`run-task.sh` default (`POSTGRES_PASSWORD:-password`) and the `DATABASE_URI` in §4.3.
|
|
128
|
-
Binaries land in `C:\Program Files\PostgreSQL\17\bin`, which is where `psql` and
|
|
129
|
-
`pg_restore` come from below.
|
|
130
|
-
|
|
131
|
-
Then seed the template DB — `./seed.sh` in the run directory does this:
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
curl -o employees.backup https://storage.mcpmark.ai/postgres/employees.backup # 33 MB
|
|
135
|
-
psql -U postgres -d postgres -c "CREATE DATABASE employees;"
|
|
136
|
-
pg_restore -U postgres -d employees --no-owner --no-privileges employees.backup
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
MCPMark's own `postgres_state_manager.py::_setup_database` does exactly this for five
|
|
140
|
-
templates (`employees`, `chinook`, `dvdrental`, `sports`, `lego`). We only need one.
|
|
141
|
-
|
|
142
|
-
### 4.2 A per-trial database
|
|
143
|
-
|
|
144
|
-
MCPMark isolates each run with `CREATE DATABASE … WITH TEMPLATE …` and drops it afterwards.
|
|
145
|
-
Reproduce that — it is what makes trials independent and `pass^4` meaningful. `./reset-db.sh`:
|
|
146
|
-
|
|
147
|
-
```sql
|
|
148
|
-
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
|
149
|
-
WHERE datname = 'hys_trial' AND pid <> pg_backend_pid();
|
|
150
|
-
DROP DATABASE IF EXISTS hys_trial;
|
|
151
|
-
CREATE DATABASE hys_trial WITH TEMPLATE employees;
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
The `pg_terminate_backend` line is not optional: `postgres-mcp` holds a pooled connection,
|
|
155
|
-
and `DROP DATABASE` fails while any session is attached. `reset-db.sh` then asserts
|
|
156
|
-
`hiring_year_summary` is absent and exits non-zero if it is not.
|
|
157
|
-
|
|
158
|
-
**Do not skip this.** Without a reset, trial 2 finds `employees.hiring_year_summary`
|
|
159
|
-
already present and the task is no longer the task.
|
|
160
|
-
|
|
161
|
-
### 4.3 The run directory
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
mkdir -p ~/Desktop/BasIns/mcpmark-hys && cd ~/Desktop/BasIns/mcpmark-hys
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Write `.mcp.json` **unwrapped** — `bir install` wraps it:
|
|
168
|
-
|
|
169
|
-
```json
|
|
170
|
-
{
|
|
171
|
-
"mcpServers": {
|
|
172
|
-
"postgres": {
|
|
173
|
-
"command": "uvx",
|
|
174
|
-
"args": ["--with", "mcp<2", "postgres-mcp==0.3.0", "--access-mode=unrestricted"],
|
|
175
|
-
"env": {
|
|
176
|
-
"DATABASE_URI": "postgresql://postgres:password@localhost:5432/hys_trial"
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
`--access-mode=unrestricted` is required — the task creates a table.
|
|
184
|
-
|
|
185
|
-
**`--with "mcp<2"` is mandatory, and MCPMark's own invocation is broken without it.**
|
|
186
|
-
`postgres-mcp==0.3.0` imports `mcp.server.fastmcp`, which no longer exists in the `mcp`
|
|
187
|
-
2.x SDK (`FastMCP` was renamed to `MCPServer`). A bare
|
|
188
|
-
`pipx run postgres-mcp==0.3.0` — MCPMark's canonical command at
|
|
189
|
-
`src/agents/mcpmark_agent.py:1183` — resolves `mcp` 2.x today and dies at import:
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where
|
|
193
|
-
FastMCP was renamed to MCPServer … or pin 'mcp<2' to keep running v1 code.
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
This is upstream drift, not our bug, but it means any MCPMark postgres result published
|
|
197
|
-
today used a pinned resolution. `uvx` is used in place of `pipx` because this machine has
|
|
198
|
-
no `pipx`; both are equivalent here.
|
|
199
|
-
|
|
200
|
-
Then:
|
|
201
|
-
|
|
202
|
-
```bash
|
|
203
|
-
bir install --replay --local
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
That wraps `postgres` with `bir-proxy`, installs the hooks into `.claude/settings.json`, and
|
|
207
|
-
adds the `bir` scenario server that direct mode delivers through. Note the port it reports —
|
|
208
|
-
it picks a free one (**53411** here, not the 53455 the incident demo uses), and the hook
|
|
209
|
-
URLs in `settings.json` are written for that port.
|
|
210
|
-
|
|
211
|
-
### 4.4 Deny the built-ins — this is the whole ballgame
|
|
212
|
-
|
|
213
|
-
Claude Code will reach for `Bash` + `psql` if you let it. One built-in in the recording and
|
|
214
|
-
`modeFor` ([coverage.ts:52](../src/replay/coverage.ts#L52)) returns `steer`, and we already
|
|
215
|
-
measured what steer mode is worth: nothing. On Windows `PowerShell` is a separate built-in
|
|
216
|
-
from `Bash` and reaches `psql` just as easily, so it must be denied too. The subagent tool is
|
|
217
|
-
`Agent` in current Claude Code; `Task` is its old name, kept for older builds. Add to
|
|
218
|
-
`.claude/settings.json`:
|
|
219
|
-
|
|
220
|
-
```json
|
|
221
|
-
{
|
|
222
|
-
"permissions": {
|
|
223
|
-
"deny": ["Bash", "PowerShell", "Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep",
|
|
224
|
-
"WebFetch", "WebSearch", "Agent", "Task"],
|
|
225
|
-
"allow": ["mcp__postgres__*", "mcp__bir__*"]
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
**The workspace must also be trusted, or the `allow` half is silently discarded** and
|
|
231
|
-
direct mode cannot work at all. Claude Code prints it once and then carries on:
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
Ignoring 2 permissions.allow entries from .claude/settings.json:
|
|
235
|
-
this workspace has not been trusted.
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
This is not cosmetic, and it fails *asymmetrically* in the worst possible way:
|
|
239
|
-
|
|
240
|
-
- `mcp__postgres__*` keeps working, because bir's own `PreToolUse` answers
|
|
241
|
-
`permissionDecision: "allow"` on the correlation path ([server.ts:859](../src/control/server.ts#L859)).
|
|
242
|
-
- `mcp__bir__run_scenario` is the **one** tool bir deliberately passes through
|
|
243
|
-
([controller.ts:301](../src/replay/controller.ts#L301) → `{kind:"passthrough"}`), so it gets
|
|
244
|
-
no hook-granted allow. With the allow list gone it is denied, and in `-p` mode a denial is
|
|
245
|
-
final:
|
|
246
|
-
|
|
247
|
-
```
|
|
248
|
-
Claude requested permissions to use mcp__bir__run_scenario,
|
|
249
|
-
but you haven't granted it yet.
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
The plan then arms in `direct` mode, the model cannot reach the delivery tool, it does the
|
|
253
|
-
task by hand, and the first manual call triggers divergence. Everything *looks* healthy —
|
|
254
|
-
the task passes and the audit log says `mode=direct` — while the saving is negative. §11 is
|
|
255
|
-
the measurement.
|
|
256
|
-
|
|
257
|
-
Fix before the first replay, either by running `claude` interactively in the directory once
|
|
258
|
-
and accepting the trust dialog, or by setting in `~/.claude.json`:
|
|
259
|
-
|
|
260
|
-
```json
|
|
261
|
-
"projects": { "C:/Users/Admin/Desktop/BasIns/mcpmark-hys": { "hasTrustDialogAccepted": true } }
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### 4.5 Start the control server, in this directory
|
|
265
|
-
|
|
266
|
-
```bash
|
|
267
|
-
export BIR_AUTH_URL=http://127.0.0.1:8080
|
|
268
|
-
export BIR_REPLAY=1
|
|
269
|
-
bir-hooks
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
Then, in another shell **in the same directory**:
|
|
273
|
-
|
|
274
|
-
```bash
|
|
275
|
-
bir doctor
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Required before going further:
|
|
279
|
-
|
|
280
|
-
```
|
|
281
|
-
Wrapped in config : postgres
|
|
282
|
-
Registered proxies: postgres ← needs claude running
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
`Registered proxies: (none)` means no proxy has bound. Launch `claude` in this directory
|
|
286
|
-
first, then re-check. **Do not send the prompt until this line is right** — that is the exact
|
|
287
|
-
mistake that made the incident-demo run cost more than the original.
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
## 5. Run protocol
|
|
292
|
-
|
|
293
|
-
The run directory is `C:\Users\Admin\Desktop\BasIns\mcpmark-hys`, and it carries four
|
|
294
|
-
scripts and a prompt file so a trial is one command:
|
|
295
|
-
|
|
296
|
-
| Script | Does |
|
|
297
|
-
|---|---|
|
|
298
|
-
| `seed.sh` | one-time: create the `employees` template DB and `pg_restore` the backup |
|
|
299
|
-
| `reset-db.sh` | per-trial: recreate `hys_trial` from the template, assert the target table is gone |
|
|
300
|
-
| `verify.sh` | MCPMark's unmodified `verify.py` plus the env it needs |
|
|
301
|
-
| `trial.sh N` | reset → `claude -p "$(cat prompt.txt)"` → verify → verdict |
|
|
302
|
-
| `prompt.txt` | the assembled prompt from §2, byte-exact |
|
|
303
|
-
|
|
304
|
-
`trial.sh` drives the agent with `claude -p`, non-interactive, one call per trial. That is
|
|
305
|
-
the same shape the τ²-bench adapter uses and it keeps the prompt byte-identical across
|
|
306
|
-
trials without a human retyping it.
|
|
307
|
-
|
|
308
|
-
### Trial 1 — record
|
|
309
|
-
|
|
310
|
-
```bash
|
|
311
|
-
./trial.sh 1
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
`trial.sh` ends by running `./verify.sh`, which is MCPMark's unmodified `verify.py` plus the
|
|
315
|
-
environment it needs:
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
PYTHONIOENCODING=utf-8 \
|
|
319
|
-
POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DATABASE=hys_trial \
|
|
320
|
-
POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=password \
|
|
321
|
-
uv run --with psycopg2-binary python verify.py
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
Exit 0 is a pass.
|
|
325
|
-
|
|
326
|
-
**`PYTHONIOENCODING=utf-8` is required on this machine.** `verify.py` prints `✅`, `❌` and
|
|
327
|
-
`🎉`, and this console's codepage is cp1255 — without it Python raises
|
|
328
|
-
`UnicodeEncodeError: 'charmap' codec can't encode character '❌'` *while formatting its
|
|
329
|
-
own result*, on both the PASS and FAIL paths. A run graded through a crashed reporter is a
|
|
330
|
-
run with no grade.
|
|
331
|
-
|
|
332
|
-
**A failed trial 1 is a stop, not a retry.** There is no point calculating a scenario from a
|
|
333
|
-
run that did not solve the task.
|
|
334
|
-
|
|
335
|
-
### Calculate the scenario
|
|
336
|
-
|
|
337
|
-
```bash
|
|
338
|
-
bir scenario list # find the runId
|
|
339
|
-
bir scenario calc <runId>
|
|
340
|
-
bir scenario show <runId> # read the intent, params and steps before trusting it
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
Read the step logic. Specifically check that `tool_output_logic` navigates
|
|
344
|
-
`data.content[0].text` and that the steps thread through `respParams` rather than every step
|
|
345
|
-
carrying a hardcoded literal. In the incident scenario every step had a baked-in fallback
|
|
346
|
-
(`?? "inventory-svc"`), which is why it looked healthy while threading nothing.
|
|
347
|
-
|
|
348
|
-
### Trials 2–4 — replay
|
|
349
|
-
|
|
350
|
-
Reset the DB, send the identical prompt, verify. Between each, confirm:
|
|
351
|
-
|
|
352
|
-
```bash
|
|
353
|
-
curl -s -H "Authorization: Bearer $(...)" http://127.0.0.1:53411/health
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
Wanted: `mode: "direct"`, `stepsPinned == stepsPlanned`, `outcome: "steered_full"`.
|
|
357
|
-
|
|
358
|
-
You can also drive a replay without a session at all, which is the fastest way to debug the
|
|
359
|
-
step logic:
|
|
360
|
-
|
|
361
|
-
```bash
|
|
362
|
-
bir replay --scenario <scnId> --prompt "<the prompt>" --dry # recorded outputs, no real SQL
|
|
363
|
-
bir replay --scenario <scnId> --prompt "<the prompt>" # real SQL through the proxy
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
`--dry` books no execution and invents no saving.
|
|
367
|
-
|
|
368
|
-
---
|
|
369
|
-
|
|
370
|
-
## 6. What to measure
|
|
371
|
-
|
|
372
|
-
| Metric | Where from | Expectation |
|
|
373
|
-
|---|---|---|
|
|
374
|
-
| pass@1 | `verify.py` exit code, trial 1 | unchanged by us |
|
|
375
|
-
| pass^4 | all four trials pass | this is the headline |
|
|
376
|
-
| `session_cost_usd` per trial | `scenario_executions` | trials 2–4 ≪ trial 1 |
|
|
377
|
-
| `saved_usd` | `scenario_executions` | **must be positive** |
|
|
378
|
-
| `duration_ms` vs `baseline_ms` | same table | secondary |
|
|
379
|
-
| `steps_pinned` / `steps_planned` | same table | must be 8/8-shaped, not 1/8 |
|
|
380
|
-
|
|
381
|
-
**Baseline honesty.** `baseline_method` is `single` and only `not_steered`/`failed` outcomes
|
|
382
|
-
feed new samples, so a scenario that keeps succeeding never widens its baseline. One sample
|
|
383
|
-
with ±8% run variance cannot support a claim to two significant figures. Run trial 1 several
|
|
384
|
-
times with `BIR_REPLAY=0` first and take a median, or state the sample size.
|
|
385
|
-
|
|
386
|
-
**`steps_pinned` is currently wrong in steer mode** —
|
|
387
|
-
[controller.ts:326](../src/replay/controller.ts#L326) uses `pinned.size`, which
|
|
388
|
-
[controller.ts:383](../src/replay/controller.ts#L383) has already decremented, so sequential
|
|
389
|
-
pins report 1. Direct mode uses a cumulative count and is fine. If this task lands in direct
|
|
390
|
-
mode the number is trustworthy; if it lands in steer mode, do not read it.
|
|
391
|
-
|
|
392
|
-
---
|
|
393
|
-
|
|
394
|
-
## 7. The honesty section
|
|
395
|
-
|
|
396
|
-
`pass^4` exists to measure whether a *model* is consistent. Raising it by not re-deciding is
|
|
397
|
-
real product value and a real user benefit — but it is not a model-capability result, and
|
|
398
|
-
publishing it as one would be indefensible.
|
|
399
|
-
|
|
400
|
-
Report it as **cost and latency at held-constant `pass^4`**, with replay disclosed as a
|
|
401
|
-
system-level intervention and trial 1 shown separately as the recording cost. That is the
|
|
402
|
-
same standard t-bench.md §3.2 already holds itself to, and it is the framing that survives
|
|
403
|
-
review.
|
|
404
|
-
|
|
405
|
-
---
|
|
406
|
-
|
|
407
|
-
## 8. Known risks
|
|
408
|
-
|
|
409
|
-
| Risk | Mitigation |
|
|
410
|
-
|---|---|
|
|
411
|
-
| Model uses `Bash`+`psql` → steer mode → zero saving | §4.4 deny list; verify `mode: "direct"` on `/health` |
|
|
412
|
-
| Recording made without the proxy → output logic authored against the hook's envelope, threads `{}` | `bir doctor` must show `Registered proxies: postgres` **before** trial 1 |
|
|
413
|
-
| No DB reset between trials | §4.2, every trial |
|
|
414
|
-
| Windows long paths — the repo will not check out | `git clone -c core.longpaths=true` |
|
|
415
|
-
| `pipx` unavailable | `uvx postgres-mcp==0.3.0`, disclosed as a deviation |
|
|
416
|
-
| Scenario expiry | `scenarios.expires_at` — re-`calc` if trials span days |
|
|
417
|
-
|
|
418
|
-
## 9. Next, if this works
|
|
419
|
-
|
|
420
|
-
Widen within `postgres` first — 31 prompts, same server, same reset mechanism, so the
|
|
421
|
-
marginal cost per task is near zero. `tasks/postgres/standard/**` is where the paper's
|
|
422
|
-
numbers come from.
|
|
423
|
-
|
|
424
|
-
Hold `filesystem` (40 prompts) back: the model prefers built-in `Read`/`Edit` over the
|
|
425
|
-
filesystem MCP server, so those tasks fight §4.4 rather than benefiting from it.
|
|
426
|
-
|
|
427
|
-
---
|
|
428
|
-
|
|
429
|
-
## 10. Repo facts worth keeping
|
|
430
|
-
|
|
431
|
-
- Layout: `tasks/<service>/<difficulty>/<initial-state>/<task-name>/{description.md,meta.json,verify.py}`
|
|
432
|
-
- 177 prompts total: **127 `standard`** (the paper's number) + 50 `easy`
|
|
433
|
-
- Per service: filesystem 40 · notion 38 · github 33 · playwright_webarena 31 · postgres 31 · playwright 4 · insforge/supabase 0
|
|
434
|
-
- The repo has grown past the paper's five servers — `insforge`, `supabase` and `playwright_webarena` are new
|
|
435
|
-
- `meta.json` carries an `"mcp": ["postgres"]` array — use it to select only tasks that can reach direct mode
|
|
436
|
-
- Small upstream bug: the §2 suffix's docstring says "Notion-specific additions" but the
|
|
437
|
-
method is on `BaseTaskManager` with no override, so it is appended for every service
|
|
438
|
-
|
|
439
|
-
---
|
|
440
|
-
|
|
441
|
-
## 11. First run: measured results (2026-09-04)
|
|
442
|
-
|
|
443
|
-
Six trials on `hiring_year_summary`. Setup as in §4, on PostgreSQL 17 native.
|
|
444
|
-
|
|
445
|
-
### Grading
|
|
446
|
-
|
|
447
|
-
**6/6 PASS.** `verify.py` reported `✅ 16 records correct` every time, against ground truth
|
|
448
|
-
it recomputes in SQL at verify time. `pass^6` on this task.
|
|
449
|
-
|
|
450
|
-
Do **not** compare that to the paper's 33.86% `pass^4`. This is an `easy`-bucket task; the
|
|
451
|
-
paper's number is over the 127 `standard` tasks. It says the harness works, not that we beat
|
|
452
|
-
anybody.
|
|
453
|
-
|
|
454
|
-
### Cost
|
|
455
|
-
|
|
456
|
-
Baseline: **$0.22401525**, 25,705 ms, `baseline_samples=1`, method `single`.
|
|
457
|
-
|
|
458
|
-
| trial | outcome | cost | saved | duration | steps |
|
|
459
|
-
|---|---|---|---|---|---|
|
|
460
|
-
| 1 | *recorded* | $0.22402 | — | 25,705 ms | 8 |
|
|
461
|
-
| 2 | `diverged` | $0.23484 | **−$0.01083** | 36,734 ms | 8/8 |
|
|
462
|
-
| 3 | `diverged` | $0.24438 | **−$0.02037** | 40,768 ms | 8/8 |
|
|
463
|
-
| 4 | `diverged` | $0.24277 | **−$0.01876** | 36,243 ms | 8/8 |
|
|
464
|
-
| 5 | `diverged` | $0.23937 | **−$0.01536** | 43,078 ms | 8/8 |
|
|
465
|
-
| 6 | `diverged` | $0.24512 | **−$0.02110** | 35,810 ms | 8/8 |
|
|
466
|
-
|
|
467
|
-
Mean saving **−$0.01728 (−7.7%)**, and every replay was *slower* than the recording.
|
|
468
|
-
Five for five negative — this is not variance.
|
|
469
|
-
|
|
470
|
-
### What worked
|
|
471
|
-
|
|
472
|
-
- **Arming is correct.** `mode=direct`, `coverage=direct,direct,direct,direct,direct,direct,direct,direct`,
|
|
473
|
-
`similarity=1.000` on every trial. This is the piece the incident demo never reached.
|
|
474
|
-
- **The recording is correct.** `wrapped=["postgres"]` at run start, outputs captured in the
|
|
475
|
-
proxy's `{"content":[...]}` envelope, 8/8 tool calls `mcp__postgres__*` and zero built-ins.
|
|
476
|
-
- **State genuinely threads.** Steps 2–4 read `respParams.userSchemas` / `respParams.tableNames`;
|
|
477
|
-
the audit log shows `emitted=allSchemas,userSchemas`, `emitted=schemaObjects,tableNames`,
|
|
478
|
-
`emitted=objectDetails,employeeColumns`. Not the incident scenario's hardcoded fallbacks.
|
|
479
|
-
- **Derivation is free.** `replay.derived params=5 costUsd=0.0000 source="recorded samples"`.
|
|
480
|
-
No `ANTHROPIC_API_KEY` needed for an identical prompt, as §2 predicted.
|
|
481
|
-
- **`steps_pinned` is right here** — 8/8 on every trial. The `pinned.size` bug in §6 is
|
|
482
|
-
specific to steer mode; the direct and divergence paths use a cumulative count.
|
|
483
|
-
|
|
484
|
-
### Why the saving is negative
|
|
485
|
-
|
|
486
|
-
`mcp__bir__run_scenario` was **denied by the permission system** on every trial — see §4.4.
|
|
487
|
-
The workspace was untrusted, so the `allow` list was discarded, and that tool is the only one
|
|
488
|
-
bir passes through rather than auto-allowing. The model could not reach the plan, so it did
|
|
489
|
-
the task by hand, and its first call diverged.
|
|
490
|
-
|
|
491
|
-
**Divergence recovery then performed exactly as designed** — and this is worth stating
|
|
492
|
-
plainly, because it is the safety net doing its job:
|
|
493
|
-
|
|
494
|
-
```
|
|
495
|
-
replay.compose remaining=8 executed=8 recorded=0 skipped=0 bytes=10259 costUsd=0.00
|
|
496
|
-
replay.done mode=direct steps=8/8 outcome=diverged ms=7591
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
All eight steps ran for real through the already-open proxy connection in ~370 ms of tool
|
|
500
|
-
time, nothing fell back to a recorded output, nothing was skipped, and the fallback cost was
|
|
501
|
-
zero. The task passed. But the model had already paid for the wasted `run_scenario` turn,
|
|
502
|
-
then paid again to read a 10,259-byte injected bundle — so the session cost landed at
|
|
503
|
-
roughly the cost of just doing the task, plus overhead.
|
|
504
|
-
|
|
505
|
-
**The mechanism is sound; the delivery channel was blocked.** Nothing in these six trials
|
|
506
|
-
measures direct mode's actual saving, because direct delivery never once happened.
|
|
507
|
-
|
|
508
|
-
### Next
|
|
509
|
-
|
|
510
|
-
1. Trust the workspace (§4.4). Without it every replay measures the divergence path.
|
|
511
|
-
2. Re-record after trusting — trials 2–6 were `diverged`, which books a ledger row but
|
|
512
|
-
never contributes a baseline sample, so the baseline is still a lone measurement of one
|
|
513
|
-
run at `$0.22401525` with ±8% run-to-run variance. Take a median of 3–5 `BIR_REPLAY=0`
|
|
514
|
-
runs before quoting any saving.
|
|
515
|
-
3. Then re-run and expect `replay.done mode=direct outcome=steered_full` with no
|
|
516
|
-
`replay.diverge` line. That is the only configuration in which a positive saving is
|
|
517
|
-
even possible.
|
|
518
|
-
|
|
519
|
-
---
|
|
520
|
-
|
|
521
|
-
## 12. Second run: direct mode reached, and the real blocker
|
|
522
|
-
|
|
523
|
-
After trusting the workspace (§4.4) and restarting `bir-hooks` with a clean session map,
|
|
524
|
-
trial 7 finally reached direct delivery:
|
|
525
|
-
|
|
526
|
-
```
|
|
527
|
-
tool.pre mcp__bir__run_scenario
|
|
528
|
-
replay.step n=0..7 all ok=true 09:53:18.924 → 09:53:19.289 (435 ms total)
|
|
529
|
-
replay.done mode=direct steps=8/8 outcome=steered_full ms=4236
|
|
530
|
-
tool.post mcp__bir__run_scenario failed=false
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
No `replay.diverge`. Every step executed for real over the already-open proxy connection in
|
|
534
|
-
435 ms of tool time, `fallbackCostUsd=0`, `deriveCostUsd=0`. `verify.py`: PASS.
|
|
535
|
-
|
|
536
|
-
### And it was the most expensive run yet
|
|
537
|
-
|
|
538
|
-
| # | outcome | cost | saved | duration |
|
|
539
|
-
|---|---|---|---|---|
|
|
540
|
-
| 1 | *recorded* | $0.22402 | — | 25,705 ms |
|
|
541
|
-
| 2 | `diverged` | $0.23484 | −$0.01083 | 36,734 ms |
|
|
542
|
-
| 3 | `diverged` | $0.24438 | −$0.02037 | 40,768 ms |
|
|
543
|
-
| 4 | `diverged` | $0.24277 | −$0.01876 | 36,243 ms |
|
|
544
|
-
| 5 | `diverged` | $0.23937 | −$0.01536 | 43,078 ms |
|
|
545
|
-
| 6 | `diverged` | $0.24512 | −$0.02110 | 35,810 ms |
|
|
546
|
-
| 7 | **`steered_full`** | **$0.31426** | **−$0.09025** | 35,921 ms |
|
|
547
|
-
|
|
548
|
-
7/7 PASS. Direct mode cost **+40% over baseline** — roughly five times the mean loss of the
|
|
549
|
-
divergence path it replaced.
|
|
550
|
-
|
|
551
|
-
### Why: the model re-did the whole task
|
|
552
|
-
|
|
553
|
-
The bundle arrived as a 10,751-byte `tool_result` on turn 3. The model read it — and then
|
|
554
|
-
ran this:
|
|
555
|
-
|
|
556
|
-
```sql
|
|
557
|
-
DROP TABLE IF EXISTS employees.hiring_year_summary;
|
|
558
|
-
CREATE TABLE employees.hiring_year_summary AS SELECT ... -- rebuilt from scratch
|
|
559
|
-
SELECT column_name, data_type FROM information_schema.columns WHERE ...
|
|
560
|
-
SELECT * FROM employees.hiring_year_summary ORDER BY hire_year;
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
It **dropped the table the replay had just created and built it again**. So the session paid
|
|
564
|
-
for the bundle *and* for doing the task, plus a longer final answer. The plan was already
|
|
565
|
-
retired by then, so nothing logged a divergence — the audit log says `steered_full` and looks
|
|
566
|
-
perfect.
|
|
567
|
-
|
|
568
|
-
### The cause is one sentence of prompt text
|
|
569
|
-
|
|
570
|
-
[bundle.ts:54](../src/replay/bundle.ts#L54):
|
|
571
|
-
|
|
572
|
-
```ts
|
|
573
|
-
"[BaseInstRunner calculated replay] A known-good tool sequence was previously " +
|
|
574
|
-
"recorded for this request. Below are the tool calls and their results, in order. " +
|
|
575
|
-
"Use them to answer the user's request now — do NOT call any tools."
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
That header is unconditional. It says **"previously recorded"** even when all eight steps
|
|
579
|
-
executed live against the real database 400 ms earlier. `anyRecorded` correctly appends a
|
|
580
|
-
staleness caveat when some step *did* fall back to a recorded output — but it never says the
|
|
581
|
-
opposite when nothing did.
|
|
582
|
-
|
|
583
|
-
For a read-only scenario that framing is harmless: the bundle contains the answer, and the
|
|
584
|
-
model just answers. For a **write** scenario it is actively counterproductive. The model is
|
|
585
|
-
told side effects are historical, cannot verify the live database without calling a tool, and
|
|
586
|
-
so reasonably re-runs the work. Every CRUD task in MCPMark — which is to say the whole
|
|
587
|
-
benchmark, its own README calls the tasks CRUD-heavy — lands in this case.
|
|
588
|
-
|
|
589
|
-
**Proposed fix:** make the header state what actually happened. When `recorded === 0`, say
|
|
590
|
-
the sequence *has just been executed against the live system and its side effects are already
|
|
591
|
-
in place*; keep the current wording only for entries that really are replayed from a
|
|
592
|
-
recording. This is the single highest-leverage change available to the product, and it is one
|
|
593
|
-
string.
|
|
594
|
-
|
|
595
|
-
*Revised by §13:* the scenario underneath these runs was broken, and that is the larger
|
|
596
|
-
problem. This header fix drops to fourth in §13's list.
|
|
597
|
-
|
|
598
|
-
Until it lands, `direct` mode's measured saving on a write task is **negative**, and no
|
|
599
|
-
amount of correct plumbing changes that — trial 7 had perfect plumbing.
|
|
600
|
-
|
|
601
|
-
### Second bug, independent of the above
|
|
602
|
-
|
|
603
|
-
`onScenarioRun` ([server.ts:1109](../src/control/server.ts#L1109)) resolves the run with:
|
|
604
|
-
|
|
605
|
-
```ts
|
|
606
|
-
const run = [...this.sessions.values()].find((s) => s.run && !s.run.finished)?.run;
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
Insertion order. A `claude -p` session's run is not sealed when the process exits, so the
|
|
610
|
-
*previous* trial's session is still first in the map, its plan retired. `runArmed` hits its
|
|
611
|
-
`state.retired` guard and answers `no_plan`, and the live armed plan at index 1 is never
|
|
612
|
-
reached. Caught on trial 4 by polling `/health` during the turn:
|
|
613
|
-
|
|
614
|
-
```
|
|
615
|
-
09:42:04 n=2 1a1b140c:direct,armed=false | 705f3cc5:direct,armed=true
|
|
616
|
-
└ picked, retired └ the actual plan, ignored
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
Workaround used here: restart `bir-hooks` before every trial. Proper fix: select the session
|
|
620
|
-
whose run carries an *armed* plan, not merely the first unfinished one.
|
|
621
|
-
|
|
622
|
-
### Also latent
|
|
623
|
-
|
|
624
|
-
`runToCompletion` calls `assembleBundle(entries, Number.MAX_SAFE_INTEGER)`
|
|
625
|
-
([plan.ts:502](../src/replay/plan.ts#L502)) — the direct-mode bundle is **uncapped**, where
|
|
626
|
-
the divergence path caps at `MAX_REPLAY_REASON` (60 KB). 10,751 bytes was fine here; a
|
|
627
|
-
scenario with large tool outputs would push an unbounded payload into the model's context.
|
|
628
|
-
|
|
629
|
-
---
|
|
630
|
-
|
|
631
|
-
## 13. The scenario was broken all along
|
|
632
|
-
|
|
633
|
-
Trial 8 is the most informative run of the set, because it is the first one where the model
|
|
634
|
-
*trusted* the bundle. It failed:
|
|
635
|
-
|
|
636
|
-
```
|
|
637
|
-
replay.done mode=direct steps=8/8 outcome=steered_full ms=3629
|
|
638
|
-
execution.reported outcome=steered_full session=0.0791 savedUsd=+0.14489850 measured=true
|
|
639
|
-
verify.py: ❌ relation "employees.hiring_year_summary" does not exist → FAIL
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
**A +$0.145 saving (−65% cost) booked on a run that produced nothing.**
|
|
643
|
-
|
|
644
|
-
### Root cause: a prose sample value interpolated into SQL
|
|
645
|
-
|
|
646
|
-
`params_object` for `scn_9832f19f`:
|
|
647
|
-
|
|
648
|
-
| parameter | sampleValue |
|
|
649
|
-
|---|---|
|
|
650
|
-
| `schema_name` | `"employees"` |
|
|
651
|
-
| `summary_table_name` | `"hiring_year_summary"` |
|
|
652
|
-
| `active_to_date` | `"9999-01-01"` |
|
|
653
|
-
| `group_by_dimension` | **`"hire_year (EXTRACT(YEAR FROM hire_date))"`** |
|
|
654
|
-
|
|
655
|
-
Step 5's input logic:
|
|
656
|
-
|
|
657
|
-
```js
|
|
658
|
-
const dim = parameters.group_by_dimension || 'hire_date';
|
|
659
|
-
... "EXTRACT(YEAR FROM e." + dim + ")::int AS hire_year," ...
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
which produces:
|
|
663
|
-
|
|
664
|
-
```sql
|
|
665
|
-
EXTRACT(YEAR FROM e.hire_year (EXTRACT(YEAR FROM hire_date)))::int AS hire_year,
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
A syntax error. `CREATE TABLE` never runs, step 6 returns `[]`, step 7 returns
|
|
669
|
-
`Error: relation "employees.hiring_year_summary" does not exist`. The SQL template itself is
|
|
670
|
-
correct — run it with `dim = 'hire_date'` and it produces the right 16 rows, verified
|
|
671
|
-
directly against psql.
|
|
672
|
-
|
|
673
|
-
The analyser wrote a **description** where a literal tool-input value was required. The
|
|
674
|
-
declared `examples` carry the same string, so nothing downstream could have caught it.
|
|
675
|
-
|
|
676
|
-
### Why six trials hid it
|
|
677
|
-
|
|
678
|
-
| trial | outcome | replay's SQL | model's behaviour | grade |
|
|
679
|
-
|---|---|---|---|---|
|
|
680
|
-
| 2–6 | `diverged` | broken | saw the errors in the bundle, did the task itself | PASS |
|
|
681
|
-
| 7 | `steered_full` | broken | `DROP TABLE` + rebuilt it correctly | PASS |
|
|
682
|
-
| 8 | `steered_full` | broken | trusted the bundle, only *proposed* SQL | **FAIL** |
|
|
683
|
-
|
|
684
|
-
**The replay has never once produced the task's result.** Every PASS came from the model
|
|
685
|
-
working around a broken scenario — which is also the real reason costs were high in §11–12,
|
|
686
|
-
and it means §12's "the model re-did the task" was the *symptom*, not the disease. The
|
|
687
|
-
model re-did the task because the bundle it received was full of SQL errors.
|
|
688
|
-
|
|
689
|
-
### Three things made this invisible from inside bir
|
|
690
|
-
|
|
691
|
-
**1. A step's verdict does not mean its work succeeded.** `executeStep` rejects only when a
|
|
692
|
-
tool could not be *run*; a tool that ran and returned an error resolves normally, so
|
|
693
|
-
`replay.step ... ok=true` is logged for a failed `CREATE TABLE`. That is a deliberate and
|
|
694
|
-
correct distinction ([controller.ts](../src/replay/controller.ts) — "a tool that ran and
|
|
695
|
-
failed resolves with its failure as the response"), but it means the audit log cannot be read
|
|
696
|
-
as evidence of success.
|
|
697
|
-
|
|
698
|
-
**2. Step 5's output logic hardcodes success:**
|
|
699
|
-
|
|
700
|
-
```js
|
|
701
|
-
return { summaryTableCreated: true, summaryTableName: ... };
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
It never inspects `toolOutput`. So `emitted=summaryTableCreated` appears in the log whether
|
|
705
|
-
or not the table exists. An output logic that asserts a fact it did not check is worse than
|
|
706
|
-
no output logic, because it manufactures corroboration.
|
|
707
|
-
|
|
708
|
-
**3. `saved_usd` is computed from cost alone.** `steered_full` means "the plan ran to
|
|
709
|
-
completion", not "the task succeeded" — and bir cannot know the benchmark grade. So the
|
|
710
|
-
cheapest possible outcome (a replay that does nothing) books the largest possible saving.
|
|
711
|
-
The signed saving in the BaseIn service's `src/scenarios/repo.ts` (a separate repo)
|
|
712
|
-
catches replays that cost *more*; nothing catches replays that cost less by doing less.
|
|
713
|
-
|
|
714
|
-
### What to fix, in order
|
|
715
|
-
|
|
716
|
-
1. **Sample values must be literal tool inputs.** The analyser prompt has to forbid prose in
|
|
717
|
-
`paramsObject.sampleValue` — it is interpolated into tool arguments verbatim. A validation
|
|
718
|
-
pass that re-evaluates each `toolInputLogic` against the recorded input and diffs the
|
|
719
|
-
result would have caught this at calculate time, for free.
|
|
720
|
-
2. **Judge a step by its output, not its resolution.** At minimum, detect an MCP result whose
|
|
721
|
-
text begins `Error:` and mark the step `failed` rather than `ok`. Today a scenario can
|
|
722
|
-
report `steered_full` while every step errored.
|
|
723
|
-
3. **Do not book a saving for a scenario whose steps errored.** Combined with (2), a step
|
|
724
|
-
marked `failed` should force `outcome: "failed"`, which already books a baseline sample
|
|
725
|
-
instead of a saving.
|
|
726
|
-
4. Then the §12 bundle-header wording, which is a real problem but a smaller one.
|
|
727
|
-
|
|
728
|
-
Until (1) is fixed, re-deriving is the immediate unblock:
|
|
729
|
-
|
|
730
|
-
```bash
|
|
731
|
-
BIR_AUTH_URL=http://127.0.0.1:8080 node <bir>/dist/bin/bir.js scenario calc <runId> --force
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
and then **check `params_object` before trusting the result** — `bir scenario show <runId>`,
|
|
735
|
-
and read every `sampleValue` as though it were about to be pasted into a shell.
|
|
736
|
-
|
|
737
|
-
---
|
|
738
|
-
|
|
739
|
-
## 14. Status (2026-09-04): every fix in §12–13 has landed
|
|
740
|
-
|
|
741
|
-
| # | Finding | Fix |
|
|
742
|
-
|---|---|---|
|
|
743
|
-
| §13 (1) | prose `sampleValue` spliced into SQL | BaseIn `analysis.ts`: the parameter prompt now requires literal example values, and `repairMismatchedInputLogic` re-evaluates every `toolInputLogic` over the sample values at calculate time and replays the recorded input verbatim when the result differs |
|
|
744
|
-
| §13 (2) | a step whose tool returned `Error:` logged `ok=true` | [tool-error.ts](../src/replay/tool-error.ts): an MCP result with `isError
|
|
745
|
-
| §13 (3) | `steered_full` booked a saving on errored steps | [controller.ts](../src/replay/controller.ts): any errored step forces `outcome: "failed"`, which books a baseline sample |
|
|
746
|
-
| §12 header | bundle said "previously recorded" for live work | [bundle.ts](../src/replay/bundle.ts): a bundle with no recorded entry says the work was just executed and its side effects are in place |
|
|
747
|
-
| §12 session | `/scenario/run` picked the first unfinished session | [server.ts](../src/control/server.ts): it picks the session whose plan is armed |
|
|
748
|
-
| §12 latent | direct-mode bundle uncapped | [plan.ts](../src/replay/plan.ts): `runToCompletion` caps at `MAX_REPLAY_REASON` |
|
|
749
|
-
| §6 | `steps_pinned` reported 1 for sequential pins | [controller.ts](../src/replay/controller.ts): counted as each pin threads, not from `pinned.size` |
|
|
750
|
-
|
|
751
|
-
Not yet re-measured. Trials 9+ need a re-`calc` (the scenario must be regenerated for the
|
|
752
|
-
validation pass to run) and then the §5 protocol again from a fresh baseline.
|
|
1
|
+
# MCPMark — running one task against BaseInstRunner
|
|
2
|
+
|
|
3
|
+
A concrete plan for one task, end to end:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
tasks/postgres/easy/employees/hiring_year_summary/
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Companion to [t-bench.md](t-bench.md). That document explains why τ²-bench was hard to
|
|
10
|
+
integrate. This one exists because **MCPMark is structurally easier and scores our product
|
|
11
|
+
more honestly**, and §1 is the reason.
|
|
12
|
+
|
|
13
|
+
Source: [github.com/eval-sys/mcpmark](https://github.com/eval-sys/mcpmark) ·
|
|
14
|
+
[paper](https://huggingface.co/papers/2509.24002) · [mcpmark.ai](https://mcpmark.ai/)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Why this benchmark, and why this task
|
|
19
|
+
|
|
20
|
+
Three structural facts, each of which τ²-bench got wrong for us.
|
|
21
|
+
|
|
22
|
+
**MCPMark grades the world, not the transcript.** `verify.py` opens a psycopg2 connection
|
|
23
|
+
and queries the actual database. τ²-bench's `evaluator_env.py` builds a *fresh* environment
|
|
24
|
+
and replays the trajectory's `(tool_call, tool_result)` pairs onto it — which is why direct
|
|
25
|
+
mode was structurally penalised there (t-bench.md §3.1). Here, direct mode executes real
|
|
26
|
+
SQL through a real connection, the table really exists, and `verify.py` passes. **Direct
|
|
27
|
+
mode is fully scoreable on MCPMark.** This is the single most important line in this
|
|
28
|
+
document.
|
|
29
|
+
|
|
30
|
+
**The agent already speaks MCP.** MCPMark spawns `postgres-mcp` over stdio
|
|
31
|
+
(`src/agents/mcpmark_agent.py:1183`). τ²-bench has no MCP interface at all, which is why it
|
|
32
|
+
needed `_shadow_tools`, `mcp-stdio-bridge.mjs` and two copies of the domain. Here
|
|
33
|
+
`bir-proxy` just wraps the server MCPMark was going to spawn anyway. **No bridge.**
|
|
34
|
+
|
|
35
|
+
**Every task runs four times against a reset state.** That is our replay shape handed over
|
|
36
|
+
for free: trial 1 records, trials 2–4 replay. And the metric MCPMark leads with, `pass^4`,
|
|
37
|
+
collapses for every model (best is 52.56% pass@1 → 33.86% pass^4) purely from run-to-run
|
|
38
|
+
inconsistency — which is exactly what a deterministic replay removes.
|
|
39
|
+
|
|
40
|
+
### Why `hiring_year_summary` specifically
|
|
41
|
+
|
|
42
|
+
| | |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Service | `postgres` — pure MCP, no built-in substitute the model would prefer |
|
|
45
|
+
| Difficulty | `L1` / `easy` bucket |
|
|
46
|
+
| Template DB | `employees` |
|
|
47
|
+
| Writes | one `CREATE TABLE` + populate — enough to be a real CRUD test, small enough to debug |
|
|
48
|
+
| Verification | ground truth computed in SQL at verify time, compared row-by-row with 0.1 tolerance on decimals |
|
|
49
|
+
| Prompt | 4 short sections, ~200 words |
|
|
50
|
+
|
|
51
|
+
It is the smallest task in the repo that still exercises write operations and per-step
|
|
52
|
+
parameter threading. Start here, then widen.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. The prompt
|
|
57
|
+
|
|
58
|
+
The user prompt is `description.md` **verbatim**, plus one fixed suffix appended by
|
|
59
|
+
`BaseTaskManager._format_task_instruction`:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
<description.md>
|
|
63
|
+
|
|
64
|
+
Note: Based on your understanding, solve the task all at once by yourself,
|
|
65
|
+
don't ask for my opinions on anything.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Plus MCPMark's system prompt (`src/agents/mcpmark_agent.py:48`, `MAX_TURNS = 100`):
|
|
69
|
+
|
|
70
|
+
> You are a helpful agent that uses tools iteratively to complete the user's task, and when
|
|
71
|
+
> finished, provides the final answer or simply states "Task completed" without further
|
|
72
|
+
> tool calls.
|
|
73
|
+
|
|
74
|
+
**No templating, no randomisation, no timestamps.** All four trials send byte-identical
|
|
75
|
+
text. Our similarity gate sees 1.0 against a 0.92 threshold, so replay arms on trials 2–4
|
|
76
|
+
every time. Nothing in this plan depends on embedding luck.
|
|
77
|
+
|
|
78
|
+
Corollary: `ANTHROPIC_API_KEY` is **not needed**. `doctor` reporting
|
|
79
|
+
`derive=recorded sample values` is correct behaviour here, not a degradation — for an
|
|
80
|
+
identical prompt the recorded values *are* the right ones.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. Topology
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
claude -p (cwd = the run dir)
|
|
88
|
+
├── mcp__bir__run_scenario ─────────────▶ bir-scenario ──┐ direct-mode delivery
|
|
89
|
+
│ │ (§4.4 — needs a trusted
|
|
90
|
+
└── mcp__postgres__* ──▶ bir-proxy ──▶ postgres-mcp │ workspace, or it is denied)
|
|
91
|
+
│ (uvx, │
|
|
92
|
+
reports every call mcp<2) │
|
|
93
|
+
▼ │ ▼
|
|
94
|
+
bir-hooks :53411 │ POST /scenario/run
|
|
95
|
+
│ │ │
|
|
96
|
+
▼ ▼ │
|
|
97
|
+
BaseIn service :8080 PostgreSQL 17 ◀┘
|
|
98
|
+
(native, :5432)
|
|
99
|
+
▲
|
|
100
|
+
verify.py ────┘
|
|
101
|
+
(grades the real database)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The proxy holds the open connection, and that is what makes direct mode free: on
|
|
105
|
+
`/scenario/run` all eight steps execute over it for zero model tokens. `bir-scenario` is
|
|
106
|
+
how the results get back into the transcript as a genuine `tool_result` rather than an
|
|
107
|
+
injected denial — which is why §4.4 matters as much as it does.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 4. Setup
|
|
112
|
+
|
|
113
|
+
### 4.1 Postgres, seeded
|
|
114
|
+
|
|
115
|
+
MCPMark runs `pgvector/pgvector:0.8.0-pg17-bookworm` in Docker. **This machine has no
|
|
116
|
+
Docker, no Docker Desktop and no WSL**, so we install PostgreSQL 17 natively instead —
|
|
117
|
+
the same major version. This task is plain SQL, so the missing `pgvector` extension is
|
|
118
|
+
irrelevant; note the deviation in any published result.
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
winget install --id PostgreSQL.PostgreSQL.17 -e `
|
|
122
|
+
--accept-package-agreements --accept-source-agreements `
|
|
123
|
+
--custom "--mode unattended --superpassword password --serverport 5432 --unattendedmodeui none"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Needs one UAC approval. The superuser password is set to `password` to match MCPMark's own
|
|
127
|
+
`run-task.sh` default (`POSTGRES_PASSWORD:-password`) and the `DATABASE_URI` in §4.3.
|
|
128
|
+
Binaries land in `C:\Program Files\PostgreSQL\17\bin`, which is where `psql` and
|
|
129
|
+
`pg_restore` come from below.
|
|
130
|
+
|
|
131
|
+
Then seed the template DB — `./seed.sh` in the run directory does this:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
curl -o employees.backup https://storage.mcpmark.ai/postgres/employees.backup # 33 MB
|
|
135
|
+
psql -U postgres -d postgres -c "CREATE DATABASE employees;"
|
|
136
|
+
pg_restore -U postgres -d employees --no-owner --no-privileges employees.backup
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
MCPMark's own `postgres_state_manager.py::_setup_database` does exactly this for five
|
|
140
|
+
templates (`employees`, `chinook`, `dvdrental`, `sports`, `lego`). We only need one.
|
|
141
|
+
|
|
142
|
+
### 4.2 A per-trial database
|
|
143
|
+
|
|
144
|
+
MCPMark isolates each run with `CREATE DATABASE … WITH TEMPLATE …` and drops it afterwards.
|
|
145
|
+
Reproduce that — it is what makes trials independent and `pass^4` meaningful. `./reset-db.sh`:
|
|
146
|
+
|
|
147
|
+
```sql
|
|
148
|
+
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
|
149
|
+
WHERE datname = 'hys_trial' AND pid <> pg_backend_pid();
|
|
150
|
+
DROP DATABASE IF EXISTS hys_trial;
|
|
151
|
+
CREATE DATABASE hys_trial WITH TEMPLATE employees;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The `pg_terminate_backend` line is not optional: `postgres-mcp` holds a pooled connection,
|
|
155
|
+
and `DROP DATABASE` fails while any session is attached. `reset-db.sh` then asserts
|
|
156
|
+
`hiring_year_summary` is absent and exits non-zero if it is not.
|
|
157
|
+
|
|
158
|
+
**Do not skip this.** Without a reset, trial 2 finds `employees.hiring_year_summary`
|
|
159
|
+
already present and the task is no longer the task.
|
|
160
|
+
|
|
161
|
+
### 4.3 The run directory
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
mkdir -p ~/Desktop/BasIns/mcpmark-hys && cd ~/Desktop/BasIns/mcpmark-hys
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Write `.mcp.json` **unwrapped** — `bir install` wraps it:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"mcpServers": {
|
|
172
|
+
"postgres": {
|
|
173
|
+
"command": "uvx",
|
|
174
|
+
"args": ["--with", "mcp<2", "postgres-mcp==0.3.0", "--access-mode=unrestricted"],
|
|
175
|
+
"env": {
|
|
176
|
+
"DATABASE_URI": "postgresql://postgres:password@localhost:5432/hys_trial"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`--access-mode=unrestricted` is required — the task creates a table.
|
|
184
|
+
|
|
185
|
+
**`--with "mcp<2"` is mandatory, and MCPMark's own invocation is broken without it.**
|
|
186
|
+
`postgres-mcp==0.3.0` imports `mcp.server.fastmcp`, which no longer exists in the `mcp`
|
|
187
|
+
2.x SDK (`FastMCP` was renamed to `MCPServer`). A bare
|
|
188
|
+
`pipx run postgres-mcp==0.3.0` — MCPMark's canonical command at
|
|
189
|
+
`src/agents/mcpmark_agent.py:1183` — resolves `mcp` 2.x today and dies at import:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where
|
|
193
|
+
FastMCP was renamed to MCPServer … or pin 'mcp<2' to keep running v1 code.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This is upstream drift, not our bug, but it means any MCPMark postgres result published
|
|
197
|
+
today used a pinned resolution. `uvx` is used in place of `pipx` because this machine has
|
|
198
|
+
no `pipx`; both are equivalent here.
|
|
199
|
+
|
|
200
|
+
Then:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
bir install --replay --local
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
That wraps `postgres` with `bir-proxy`, installs the hooks into `.claude/settings.json`, and
|
|
207
|
+
adds the `bir` scenario server that direct mode delivers through. Note the port it reports —
|
|
208
|
+
it picks a free one (**53411** here, not the 53455 the incident demo uses), and the hook
|
|
209
|
+
URLs in `settings.json` are written for that port.
|
|
210
|
+
|
|
211
|
+
### 4.4 Deny the built-ins — this is the whole ballgame
|
|
212
|
+
|
|
213
|
+
Claude Code will reach for `Bash` + `psql` if you let it. One built-in in the recording and
|
|
214
|
+
`modeFor` ([coverage.ts:52](../src/replay/coverage.ts#L52)) returns `steer`, and we already
|
|
215
|
+
measured what steer mode is worth: nothing. On Windows `PowerShell` is a separate built-in
|
|
216
|
+
from `Bash` and reaches `psql` just as easily, so it must be denied too. The subagent tool is
|
|
217
|
+
`Agent` in current Claude Code; `Task` is its old name, kept for older builds. Add to
|
|
218
|
+
`.claude/settings.json`:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"permissions": {
|
|
223
|
+
"deny": ["Bash", "PowerShell", "Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep",
|
|
224
|
+
"WebFetch", "WebSearch", "Agent", "Task"],
|
|
225
|
+
"allow": ["mcp__postgres__*", "mcp__bir__*"]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**The workspace must also be trusted, or the `allow` half is silently discarded** and
|
|
231
|
+
direct mode cannot work at all. Claude Code prints it once and then carries on:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Ignoring 2 permissions.allow entries from .claude/settings.json:
|
|
235
|
+
this workspace has not been trusted.
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This is not cosmetic, and it fails *asymmetrically* in the worst possible way:
|
|
239
|
+
|
|
240
|
+
- `mcp__postgres__*` keeps working, because bir's own `PreToolUse` answers
|
|
241
|
+
`permissionDecision: "allow"` on the correlation path ([server.ts:859](../src/control/server.ts#L859)).
|
|
242
|
+
- `mcp__bir__run_scenario` is the **one** tool bir deliberately passes through
|
|
243
|
+
([controller.ts:301](../src/replay/controller.ts#L301) → `{kind:"passthrough"}`), so it gets
|
|
244
|
+
no hook-granted allow. With the allow list gone it is denied, and in `-p` mode a denial is
|
|
245
|
+
final:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
Claude requested permissions to use mcp__bir__run_scenario,
|
|
249
|
+
but you haven't granted it yet.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The plan then arms in `direct` mode, the model cannot reach the delivery tool, it does the
|
|
253
|
+
task by hand, and the first manual call triggers divergence. Everything *looks* healthy —
|
|
254
|
+
the task passes and the audit log says `mode=direct` — while the saving is negative. §11 is
|
|
255
|
+
the measurement.
|
|
256
|
+
|
|
257
|
+
Fix before the first replay, either by running `claude` interactively in the directory once
|
|
258
|
+
and accepting the trust dialog, or by setting in `~/.claude.json`:
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
"projects": { "C:/Users/Admin/Desktop/BasIns/mcpmark-hys": { "hasTrustDialogAccepted": true } }
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### 4.5 Start the control server, in this directory
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
export BIR_AUTH_URL=http://127.0.0.1:8080
|
|
268
|
+
export BIR_REPLAY=1
|
|
269
|
+
bir-hooks
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Then, in another shell **in the same directory**:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
bir doctor
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Required before going further:
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
Wrapped in config : postgres
|
|
282
|
+
Registered proxies: postgres ← needs claude running
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`Registered proxies: (none)` means no proxy has bound. Launch `claude` in this directory
|
|
286
|
+
first, then re-check. **Do not send the prompt until this line is right** — that is the exact
|
|
287
|
+
mistake that made the incident-demo run cost more than the original.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 5. Run protocol
|
|
292
|
+
|
|
293
|
+
The run directory is `C:\Users\Admin\Desktop\BasIns\mcpmark-hys`, and it carries four
|
|
294
|
+
scripts and a prompt file so a trial is one command:
|
|
295
|
+
|
|
296
|
+
| Script | Does |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `seed.sh` | one-time: create the `employees` template DB and `pg_restore` the backup |
|
|
299
|
+
| `reset-db.sh` | per-trial: recreate `hys_trial` from the template, assert the target table is gone |
|
|
300
|
+
| `verify.sh` | MCPMark's unmodified `verify.py` plus the env it needs |
|
|
301
|
+
| `trial.sh N` | reset → `claude -p "$(cat prompt.txt)"` → verify → verdict |
|
|
302
|
+
| `prompt.txt` | the assembled prompt from §2, byte-exact |
|
|
303
|
+
|
|
304
|
+
`trial.sh` drives the agent with `claude -p`, non-interactive, one call per trial. That is
|
|
305
|
+
the same shape the τ²-bench adapter uses and it keeps the prompt byte-identical across
|
|
306
|
+
trials without a human retyping it.
|
|
307
|
+
|
|
308
|
+
### Trial 1 — record
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
./trial.sh 1
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`trial.sh` ends by running `./verify.sh`, which is MCPMark's unmodified `verify.py` plus the
|
|
315
|
+
environment it needs:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
PYTHONIOENCODING=utf-8 \
|
|
319
|
+
POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DATABASE=hys_trial \
|
|
320
|
+
POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=password \
|
|
321
|
+
uv run --with psycopg2-binary python verify.py
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Exit 0 is a pass.
|
|
325
|
+
|
|
326
|
+
**`PYTHONIOENCODING=utf-8` is required on this machine.** `verify.py` prints `✅`, `❌` and
|
|
327
|
+
`🎉`, and this console's codepage is cp1255 — without it Python raises
|
|
328
|
+
`UnicodeEncodeError: 'charmap' codec can't encode character '❌'` *while formatting its
|
|
329
|
+
own result*, on both the PASS and FAIL paths. A run graded through a crashed reporter is a
|
|
330
|
+
run with no grade.
|
|
331
|
+
|
|
332
|
+
**A failed trial 1 is a stop, not a retry.** There is no point calculating a scenario from a
|
|
333
|
+
run that did not solve the task.
|
|
334
|
+
|
|
335
|
+
### Calculate the scenario
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
bir scenario list # find the runId
|
|
339
|
+
bir scenario calc <runId>
|
|
340
|
+
bir scenario show <runId> # read the intent, params and steps before trusting it
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Read the step logic. Specifically check that `tool_output_logic` navigates
|
|
344
|
+
`data.content[0].text` and that the steps thread through `respParams` rather than every step
|
|
345
|
+
carrying a hardcoded literal. In the incident scenario every step had a baked-in fallback
|
|
346
|
+
(`?? "inventory-svc"`), which is why it looked healthy while threading nothing.
|
|
347
|
+
|
|
348
|
+
### Trials 2–4 — replay
|
|
349
|
+
|
|
350
|
+
Reset the DB, send the identical prompt, verify. Between each, confirm:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
curl -s -H "Authorization: Bearer $(...)" http://127.0.0.1:53411/health
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Wanted: `mode: "direct"`, `stepsPinned == stepsPlanned`, `outcome: "steered_full"`.
|
|
357
|
+
|
|
358
|
+
You can also drive a replay without a session at all, which is the fastest way to debug the
|
|
359
|
+
step logic:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
bir replay --scenario <scnId> --prompt "<the prompt>" --dry # recorded outputs, no real SQL
|
|
363
|
+
bir replay --scenario <scnId> --prompt "<the prompt>" # real SQL through the proxy
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`--dry` books no execution and invents no saving.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 6. What to measure
|
|
371
|
+
|
|
372
|
+
| Metric | Where from | Expectation |
|
|
373
|
+
|---|---|---|
|
|
374
|
+
| pass@1 | `verify.py` exit code, trial 1 | unchanged by us |
|
|
375
|
+
| pass^4 | all four trials pass | this is the headline |
|
|
376
|
+
| `session_cost_usd` per trial | `scenario_executions` | trials 2–4 ≪ trial 1 |
|
|
377
|
+
| `saved_usd` | `scenario_executions` | **must be positive** |
|
|
378
|
+
| `duration_ms` vs `baseline_ms` | same table | secondary |
|
|
379
|
+
| `steps_pinned` / `steps_planned` | same table | must be 8/8-shaped, not 1/8 |
|
|
380
|
+
|
|
381
|
+
**Baseline honesty.** `baseline_method` is `single` and only `not_steered`/`failed` outcomes
|
|
382
|
+
feed new samples, so a scenario that keeps succeeding never widens its baseline. One sample
|
|
383
|
+
with ±8% run variance cannot support a claim to two significant figures. Run trial 1 several
|
|
384
|
+
times with `BIR_REPLAY=0` first and take a median, or state the sample size.
|
|
385
|
+
|
|
386
|
+
**`steps_pinned` is currently wrong in steer mode** —
|
|
387
|
+
[controller.ts:326](../src/replay/controller.ts#L326) uses `pinned.size`, which
|
|
388
|
+
[controller.ts:383](../src/replay/controller.ts#L383) has already decremented, so sequential
|
|
389
|
+
pins report 1. Direct mode uses a cumulative count and is fine. If this task lands in direct
|
|
390
|
+
mode the number is trustworthy; if it lands in steer mode, do not read it.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 7. The honesty section
|
|
395
|
+
|
|
396
|
+
`pass^4` exists to measure whether a *model* is consistent. Raising it by not re-deciding is
|
|
397
|
+
real product value and a real user benefit — but it is not a model-capability result, and
|
|
398
|
+
publishing it as one would be indefensible.
|
|
399
|
+
|
|
400
|
+
Report it as **cost and latency at held-constant `pass^4`**, with replay disclosed as a
|
|
401
|
+
system-level intervention and trial 1 shown separately as the recording cost. That is the
|
|
402
|
+
same standard t-bench.md §3.2 already holds itself to, and it is the framing that survives
|
|
403
|
+
review.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 8. Known risks
|
|
408
|
+
|
|
409
|
+
| Risk | Mitigation |
|
|
410
|
+
|---|---|
|
|
411
|
+
| Model uses `Bash`+`psql` → steer mode → zero saving | §4.4 deny list; verify `mode: "direct"` on `/health` |
|
|
412
|
+
| Recording made without the proxy → output logic authored against the hook's envelope, threads `{}` | `bir doctor` must show `Registered proxies: postgres` **before** trial 1 |
|
|
413
|
+
| No DB reset between trials | §4.2, every trial |
|
|
414
|
+
| Windows long paths — the repo will not check out | `git clone -c core.longpaths=true` |
|
|
415
|
+
| `pipx` unavailable | `uvx postgres-mcp==0.3.0`, disclosed as a deviation |
|
|
416
|
+
| Scenario expiry | `scenarios.expires_at` — re-`calc` if trials span days |
|
|
417
|
+
|
|
418
|
+
## 9. Next, if this works
|
|
419
|
+
|
|
420
|
+
Widen within `postgres` first — 31 prompts, same server, same reset mechanism, so the
|
|
421
|
+
marginal cost per task is near zero. `tasks/postgres/standard/**` is where the paper's
|
|
422
|
+
numbers come from.
|
|
423
|
+
|
|
424
|
+
Hold `filesystem` (40 prompts) back: the model prefers built-in `Read`/`Edit` over the
|
|
425
|
+
filesystem MCP server, so those tasks fight §4.4 rather than benefiting from it.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## 10. Repo facts worth keeping
|
|
430
|
+
|
|
431
|
+
- Layout: `tasks/<service>/<difficulty>/<initial-state>/<task-name>/{description.md,meta.json,verify.py}`
|
|
432
|
+
- 177 prompts total: **127 `standard`** (the paper's number) + 50 `easy`
|
|
433
|
+
- Per service: filesystem 40 · notion 38 · github 33 · playwright_webarena 31 · postgres 31 · playwright 4 · insforge/supabase 0
|
|
434
|
+
- The repo has grown past the paper's five servers — `insforge`, `supabase` and `playwright_webarena` are new
|
|
435
|
+
- `meta.json` carries an `"mcp": ["postgres"]` array — use it to select only tasks that can reach direct mode
|
|
436
|
+
- Small upstream bug: the §2 suffix's docstring says "Notion-specific additions" but the
|
|
437
|
+
method is on `BaseTaskManager` with no override, so it is appended for every service
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 11. First run: measured results (2026-09-04)
|
|
442
|
+
|
|
443
|
+
Six trials on `hiring_year_summary`. Setup as in §4, on PostgreSQL 17 native.
|
|
444
|
+
|
|
445
|
+
### Grading
|
|
446
|
+
|
|
447
|
+
**6/6 PASS.** `verify.py` reported `✅ 16 records correct` every time, against ground truth
|
|
448
|
+
it recomputes in SQL at verify time. `pass^6` on this task.
|
|
449
|
+
|
|
450
|
+
Do **not** compare that to the paper's 33.86% `pass^4`. This is an `easy`-bucket task; the
|
|
451
|
+
paper's number is over the 127 `standard` tasks. It says the harness works, not that we beat
|
|
452
|
+
anybody.
|
|
453
|
+
|
|
454
|
+
### Cost
|
|
455
|
+
|
|
456
|
+
Baseline: **$0.22401525**, 25,705 ms, `baseline_samples=1`, method `single`.
|
|
457
|
+
|
|
458
|
+
| trial | outcome | cost | saved | duration | steps |
|
|
459
|
+
|---|---|---|---|---|---|
|
|
460
|
+
| 1 | *recorded* | $0.22402 | — | 25,705 ms | 8 |
|
|
461
|
+
| 2 | `diverged` | $0.23484 | **−$0.01083** | 36,734 ms | 8/8 |
|
|
462
|
+
| 3 | `diverged` | $0.24438 | **−$0.02037** | 40,768 ms | 8/8 |
|
|
463
|
+
| 4 | `diverged` | $0.24277 | **−$0.01876** | 36,243 ms | 8/8 |
|
|
464
|
+
| 5 | `diverged` | $0.23937 | **−$0.01536** | 43,078 ms | 8/8 |
|
|
465
|
+
| 6 | `diverged` | $0.24512 | **−$0.02110** | 35,810 ms | 8/8 |
|
|
466
|
+
|
|
467
|
+
Mean saving **−$0.01728 (−7.7%)**, and every replay was *slower* than the recording.
|
|
468
|
+
Five for five negative — this is not variance.
|
|
469
|
+
|
|
470
|
+
### What worked
|
|
471
|
+
|
|
472
|
+
- **Arming is correct.** `mode=direct`, `coverage=direct,direct,direct,direct,direct,direct,direct,direct`,
|
|
473
|
+
`similarity=1.000` on every trial. This is the piece the incident demo never reached.
|
|
474
|
+
- **The recording is correct.** `wrapped=["postgres"]` at run start, outputs captured in the
|
|
475
|
+
proxy's `{"content":[...]}` envelope, 8/8 tool calls `mcp__postgres__*` and zero built-ins.
|
|
476
|
+
- **State genuinely threads.** Steps 2–4 read `respParams.userSchemas` / `respParams.tableNames`;
|
|
477
|
+
the audit log shows `emitted=allSchemas,userSchemas`, `emitted=schemaObjects,tableNames`,
|
|
478
|
+
`emitted=objectDetails,employeeColumns`. Not the incident scenario's hardcoded fallbacks.
|
|
479
|
+
- **Derivation is free.** `replay.derived params=5 costUsd=0.0000 source="recorded samples"`.
|
|
480
|
+
No `ANTHROPIC_API_KEY` needed for an identical prompt, as §2 predicted.
|
|
481
|
+
- **`steps_pinned` is right here** — 8/8 on every trial. The `pinned.size` bug in §6 is
|
|
482
|
+
specific to steer mode; the direct and divergence paths use a cumulative count.
|
|
483
|
+
|
|
484
|
+
### Why the saving is negative
|
|
485
|
+
|
|
486
|
+
`mcp__bir__run_scenario` was **denied by the permission system** on every trial — see §4.4.
|
|
487
|
+
The workspace was untrusted, so the `allow` list was discarded, and that tool is the only one
|
|
488
|
+
bir passes through rather than auto-allowing. The model could not reach the plan, so it did
|
|
489
|
+
the task by hand, and its first call diverged.
|
|
490
|
+
|
|
491
|
+
**Divergence recovery then performed exactly as designed** — and this is worth stating
|
|
492
|
+
plainly, because it is the safety net doing its job:
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
replay.compose remaining=8 executed=8 recorded=0 skipped=0 bytes=10259 costUsd=0.00
|
|
496
|
+
replay.done mode=direct steps=8/8 outcome=diverged ms=7591
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
All eight steps ran for real through the already-open proxy connection in ~370 ms of tool
|
|
500
|
+
time, nothing fell back to a recorded output, nothing was skipped, and the fallback cost was
|
|
501
|
+
zero. The task passed. But the model had already paid for the wasted `run_scenario` turn,
|
|
502
|
+
then paid again to read a 10,259-byte injected bundle — so the session cost landed at
|
|
503
|
+
roughly the cost of just doing the task, plus overhead.
|
|
504
|
+
|
|
505
|
+
**The mechanism is sound; the delivery channel was blocked.** Nothing in these six trials
|
|
506
|
+
measures direct mode's actual saving, because direct delivery never once happened.
|
|
507
|
+
|
|
508
|
+
### Next
|
|
509
|
+
|
|
510
|
+
1. Trust the workspace (§4.4). Without it every replay measures the divergence path.
|
|
511
|
+
2. Re-record after trusting — trials 2–6 were `diverged`, which books a ledger row but
|
|
512
|
+
never contributes a baseline sample, so the baseline is still a lone measurement of one
|
|
513
|
+
run at `$0.22401525` with ±8% run-to-run variance. Take a median of 3–5 `BIR_REPLAY=0`
|
|
514
|
+
runs before quoting any saving.
|
|
515
|
+
3. Then re-run and expect `replay.done mode=direct outcome=steered_full` with no
|
|
516
|
+
`replay.diverge` line. That is the only configuration in which a positive saving is
|
|
517
|
+
even possible.
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## 12. Second run: direct mode reached, and the real blocker
|
|
522
|
+
|
|
523
|
+
After trusting the workspace (§4.4) and restarting `bir-hooks` with a clean session map,
|
|
524
|
+
trial 7 finally reached direct delivery:
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
tool.pre mcp__bir__run_scenario
|
|
528
|
+
replay.step n=0..7 all ok=true 09:53:18.924 → 09:53:19.289 (435 ms total)
|
|
529
|
+
replay.done mode=direct steps=8/8 outcome=steered_full ms=4236
|
|
530
|
+
tool.post mcp__bir__run_scenario failed=false
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
No `replay.diverge`. Every step executed for real over the already-open proxy connection in
|
|
534
|
+
435 ms of tool time, `fallbackCostUsd=0`, `deriveCostUsd=0`. `verify.py`: PASS.
|
|
535
|
+
|
|
536
|
+
### And it was the most expensive run yet
|
|
537
|
+
|
|
538
|
+
| # | outcome | cost | saved | duration |
|
|
539
|
+
|---|---|---|---|---|
|
|
540
|
+
| 1 | *recorded* | $0.22402 | — | 25,705 ms |
|
|
541
|
+
| 2 | `diverged` | $0.23484 | −$0.01083 | 36,734 ms |
|
|
542
|
+
| 3 | `diverged` | $0.24438 | −$0.02037 | 40,768 ms |
|
|
543
|
+
| 4 | `diverged` | $0.24277 | −$0.01876 | 36,243 ms |
|
|
544
|
+
| 5 | `diverged` | $0.23937 | −$0.01536 | 43,078 ms |
|
|
545
|
+
| 6 | `diverged` | $0.24512 | −$0.02110 | 35,810 ms |
|
|
546
|
+
| 7 | **`steered_full`** | **$0.31426** | **−$0.09025** | 35,921 ms |
|
|
547
|
+
|
|
548
|
+
7/7 PASS. Direct mode cost **+40% over baseline** — roughly five times the mean loss of the
|
|
549
|
+
divergence path it replaced.
|
|
550
|
+
|
|
551
|
+
### Why: the model re-did the whole task
|
|
552
|
+
|
|
553
|
+
The bundle arrived as a 10,751-byte `tool_result` on turn 3. The model read it — and then
|
|
554
|
+
ran this:
|
|
555
|
+
|
|
556
|
+
```sql
|
|
557
|
+
DROP TABLE IF EXISTS employees.hiring_year_summary;
|
|
558
|
+
CREATE TABLE employees.hiring_year_summary AS SELECT ... -- rebuilt from scratch
|
|
559
|
+
SELECT column_name, data_type FROM information_schema.columns WHERE ...
|
|
560
|
+
SELECT * FROM employees.hiring_year_summary ORDER BY hire_year;
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
It **dropped the table the replay had just created and built it again**. So the session paid
|
|
564
|
+
for the bundle *and* for doing the task, plus a longer final answer. The plan was already
|
|
565
|
+
retired by then, so nothing logged a divergence — the audit log says `steered_full` and looks
|
|
566
|
+
perfect.
|
|
567
|
+
|
|
568
|
+
### The cause is one sentence of prompt text
|
|
569
|
+
|
|
570
|
+
[bundle.ts:54](../src/replay/bundle.ts#L54):
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
"[BaseInstRunner calculated replay] A known-good tool sequence was previously " +
|
|
574
|
+
"recorded for this request. Below are the tool calls and their results, in order. " +
|
|
575
|
+
"Use them to answer the user's request now — do NOT call any tools."
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
That header is unconditional. It says **"previously recorded"** even when all eight steps
|
|
579
|
+
executed live against the real database 400 ms earlier. `anyRecorded` correctly appends a
|
|
580
|
+
staleness caveat when some step *did* fall back to a recorded output — but it never says the
|
|
581
|
+
opposite when nothing did.
|
|
582
|
+
|
|
583
|
+
For a read-only scenario that framing is harmless: the bundle contains the answer, and the
|
|
584
|
+
model just answers. For a **write** scenario it is actively counterproductive. The model is
|
|
585
|
+
told side effects are historical, cannot verify the live database without calling a tool, and
|
|
586
|
+
so reasonably re-runs the work. Every CRUD task in MCPMark — which is to say the whole
|
|
587
|
+
benchmark, its own README calls the tasks CRUD-heavy — lands in this case.
|
|
588
|
+
|
|
589
|
+
**Proposed fix:** make the header state what actually happened. When `recorded === 0`, say
|
|
590
|
+
the sequence *has just been executed against the live system and its side effects are already
|
|
591
|
+
in place*; keep the current wording only for entries that really are replayed from a
|
|
592
|
+
recording. This is the single highest-leverage change available to the product, and it is one
|
|
593
|
+
string.
|
|
594
|
+
|
|
595
|
+
*Revised by §13:* the scenario underneath these runs was broken, and that is the larger
|
|
596
|
+
problem. This header fix drops to fourth in §13's list.
|
|
597
|
+
|
|
598
|
+
Until it lands, `direct` mode's measured saving on a write task is **negative**, and no
|
|
599
|
+
amount of correct plumbing changes that — trial 7 had perfect plumbing.
|
|
600
|
+
|
|
601
|
+
### Second bug, independent of the above
|
|
602
|
+
|
|
603
|
+
`onScenarioRun` ([server.ts:1109](../src/control/server.ts#L1109)) resolves the run with:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
const run = [...this.sessions.values()].find((s) => s.run && !s.run.finished)?.run;
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Insertion order. A `claude -p` session's run is not sealed when the process exits, so the
|
|
610
|
+
*previous* trial's session is still first in the map, its plan retired. `runArmed` hits its
|
|
611
|
+
`state.retired` guard and answers `no_plan`, and the live armed plan at index 1 is never
|
|
612
|
+
reached. Caught on trial 4 by polling `/health` during the turn:
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
09:42:04 n=2 1a1b140c:direct,armed=false | 705f3cc5:direct,armed=true
|
|
616
|
+
└ picked, retired └ the actual plan, ignored
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Workaround used here: restart `bir-hooks` before every trial. Proper fix: select the session
|
|
620
|
+
whose run carries an *armed* plan, not merely the first unfinished one.
|
|
621
|
+
|
|
622
|
+
### Also latent
|
|
623
|
+
|
|
624
|
+
`runToCompletion` calls `assembleBundle(entries, Number.MAX_SAFE_INTEGER)`
|
|
625
|
+
([plan.ts:502](../src/replay/plan.ts#L502)) — the direct-mode bundle is **uncapped**, where
|
|
626
|
+
the divergence path caps at `MAX_REPLAY_REASON` (60 KB). 10,751 bytes was fine here; a
|
|
627
|
+
scenario with large tool outputs would push an unbounded payload into the model's context.
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## 13. The scenario was broken all along
|
|
632
|
+
|
|
633
|
+
Trial 8 is the most informative run of the set, because it is the first one where the model
|
|
634
|
+
*trusted* the bundle. It failed:
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
replay.done mode=direct steps=8/8 outcome=steered_full ms=3629
|
|
638
|
+
execution.reported outcome=steered_full session=0.0791 savedUsd=+0.14489850 measured=true
|
|
639
|
+
verify.py: ❌ relation "employees.hiring_year_summary" does not exist → FAIL
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**A +$0.145 saving (−65% cost) booked on a run that produced nothing.**
|
|
643
|
+
|
|
644
|
+
### Root cause: a prose sample value interpolated into SQL
|
|
645
|
+
|
|
646
|
+
`params_object` for `scn_9832f19f`:
|
|
647
|
+
|
|
648
|
+
| parameter | sampleValue |
|
|
649
|
+
|---|---|
|
|
650
|
+
| `schema_name` | `"employees"` |
|
|
651
|
+
| `summary_table_name` | `"hiring_year_summary"` |
|
|
652
|
+
| `active_to_date` | `"9999-01-01"` |
|
|
653
|
+
| `group_by_dimension` | **`"hire_year (EXTRACT(YEAR FROM hire_date))"`** |
|
|
654
|
+
|
|
655
|
+
Step 5's input logic:
|
|
656
|
+
|
|
657
|
+
```js
|
|
658
|
+
const dim = parameters.group_by_dimension || 'hire_date';
|
|
659
|
+
... "EXTRACT(YEAR FROM e." + dim + ")::int AS hire_year," ...
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
which produces:
|
|
663
|
+
|
|
664
|
+
```sql
|
|
665
|
+
EXTRACT(YEAR FROM e.hire_year (EXTRACT(YEAR FROM hire_date)))::int AS hire_year,
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
A syntax error. `CREATE TABLE` never runs, step 6 returns `[]`, step 7 returns
|
|
669
|
+
`Error: relation "employees.hiring_year_summary" does not exist`. The SQL template itself is
|
|
670
|
+
correct — run it with `dim = 'hire_date'` and it produces the right 16 rows, verified
|
|
671
|
+
directly against psql.
|
|
672
|
+
|
|
673
|
+
The analyser wrote a **description** where a literal tool-input value was required. The
|
|
674
|
+
declared `examples` carry the same string, so nothing downstream could have caught it.
|
|
675
|
+
|
|
676
|
+
### Why six trials hid it
|
|
677
|
+
|
|
678
|
+
| trial | outcome | replay's SQL | model's behaviour | grade |
|
|
679
|
+
|---|---|---|---|---|
|
|
680
|
+
| 2–6 | `diverged` | broken | saw the errors in the bundle, did the task itself | PASS |
|
|
681
|
+
| 7 | `steered_full` | broken | `DROP TABLE` + rebuilt it correctly | PASS |
|
|
682
|
+
| 8 | `steered_full` | broken | trusted the bundle, only *proposed* SQL | **FAIL** |
|
|
683
|
+
|
|
684
|
+
**The replay has never once produced the task's result.** Every PASS came from the model
|
|
685
|
+
working around a broken scenario — which is also the real reason costs were high in §11–12,
|
|
686
|
+
and it means §12's "the model re-did the task" was the *symptom*, not the disease. The
|
|
687
|
+
model re-did the task because the bundle it received was full of SQL errors.
|
|
688
|
+
|
|
689
|
+
### Three things made this invisible from inside bir
|
|
690
|
+
|
|
691
|
+
**1. A step's verdict does not mean its work succeeded.** `executeStep` rejects only when a
|
|
692
|
+
tool could not be *run*; a tool that ran and returned an error resolves normally, so
|
|
693
|
+
`replay.step ... ok=true` is logged for a failed `CREATE TABLE`. That is a deliberate and
|
|
694
|
+
correct distinction ([controller.ts](../src/replay/controller.ts) — "a tool that ran and
|
|
695
|
+
failed resolves with its failure as the response"), but it means the audit log cannot be read
|
|
696
|
+
as evidence of success.
|
|
697
|
+
|
|
698
|
+
**2. Step 5's output logic hardcodes success:**
|
|
699
|
+
|
|
700
|
+
```js
|
|
701
|
+
return { summaryTableCreated: true, summaryTableName: ... };
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
It never inspects `toolOutput`. So `emitted=summaryTableCreated` appears in the log whether
|
|
705
|
+
or not the table exists. An output logic that asserts a fact it did not check is worse than
|
|
706
|
+
no output logic, because it manufactures corroboration.
|
|
707
|
+
|
|
708
|
+
**3. `saved_usd` is computed from cost alone.** `steered_full` means "the plan ran to
|
|
709
|
+
completion", not "the task succeeded" — and bir cannot know the benchmark grade. So the
|
|
710
|
+
cheapest possible outcome (a replay that does nothing) books the largest possible saving.
|
|
711
|
+
The signed saving in the BaseIn service's `src/scenarios/repo.ts` (a separate repo)
|
|
712
|
+
catches replays that cost *more*; nothing catches replays that cost less by doing less.
|
|
713
|
+
|
|
714
|
+
### What to fix, in order
|
|
715
|
+
|
|
716
|
+
1. **Sample values must be literal tool inputs.** The analyser prompt has to forbid prose in
|
|
717
|
+
`paramsObject.sampleValue` — it is interpolated into tool arguments verbatim. A validation
|
|
718
|
+
pass that re-evaluates each `toolInputLogic` against the recorded input and diffs the
|
|
719
|
+
result would have caught this at calculate time, for free.
|
|
720
|
+
2. **Judge a step by its output, not its resolution.** At minimum, detect an MCP result whose
|
|
721
|
+
text begins `Error:` and mark the step `failed` rather than `ok`. Today a scenario can
|
|
722
|
+
report `steered_full` while every step errored.
|
|
723
|
+
3. **Do not book a saving for a scenario whose steps errored.** Combined with (2), a step
|
|
724
|
+
marked `failed` should force `outcome: "failed"`, which already books a baseline sample
|
|
725
|
+
instead of a saving.
|
|
726
|
+
4. Then the §12 bundle-header wording, which is a real problem but a smaller one.
|
|
727
|
+
|
|
728
|
+
Until (1) is fixed, re-deriving is the immediate unblock:
|
|
729
|
+
|
|
730
|
+
```bash
|
|
731
|
+
BIR_AUTH_URL=http://127.0.0.1:8080 node <bir>/dist/bin/bir.js scenario calc <runId> --force
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
and then **check `params_object` before trusting the result** — `bir scenario show <runId>`,
|
|
735
|
+
and read every `sampleValue` as though it were about to be pasted into a shell.
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## 14. Status (2026-09-04): every fix in §12–13 has landed
|
|
740
|
+
|
|
741
|
+
| # | Finding | Fix |
|
|
742
|
+
|---|---|---|
|
|
743
|
+
| §13 (1) | prose `sampleValue` spliced into SQL | BaseIn `analysis.ts`: the parameter prompt now requires literal example values, and `repairMismatchedInputLogic` re-evaluates every `toolInputLogic` over the sample values at calculate time and replays the recorded input verbatim when the result differs |
|
|
744
|
+
| §13 (2) | a step whose tool returned `Error:` logged `ok=true` | [tool-error.ts](../src/replay/tool-error.ts): an MCP result with `isError: true` is a `failed` step at stage `tool_call`, in direct, divergence and steer mode alike. Since 2026-09-19 a leading `Error` text with `isError: false` no longer counts: replay judges a step by the same rule the recorder does |
|
|
745
|
+
| §13 (3) | `steered_full` booked a saving on errored steps | [controller.ts](../src/replay/controller.ts): any errored step forces `outcome: "failed"`, which books a baseline sample |
|
|
746
|
+
| §12 header | bundle said "previously recorded" for live work | [bundle.ts](../src/replay/bundle.ts): a bundle with no recorded entry says the work was just executed and its side effects are in place |
|
|
747
|
+
| §12 session | `/scenario/run` picked the first unfinished session | [server.ts](../src/control/server.ts): it picks the session whose plan is armed |
|
|
748
|
+
| §12 latent | direct-mode bundle uncapped | [plan.ts](../src/replay/plan.ts): `runToCompletion` caps at `MAX_REPLAY_REASON` |
|
|
749
|
+
| §6 | `steps_pinned` reported 1 for sequential pins | [controller.ts](../src/replay/controller.ts): counted as each pin threads, not from `pinned.size` |
|
|
750
|
+
|
|
751
|
+
Not yet re-measured. Trials 9+ need a re-`calc` (the scenario must be regenerated for the
|
|
752
|
+
validation pass to run) and then the §5 protocol again from a fresh baseline.
|