Troubleshooting
Each problem is listed by what Gadfly prints. A refusal prints what the last line of a Python traceback would, its class, which says what to fix and gives the exit status, and what went wrong, then on the next line what to do; a warning, for what a run did not do as asked though it went on, prints the same way. With --json a refusal is one object on standard error and a warning sits under warnings in the run's result, as each table below can show on the site.
Exit statuses
The status says what happened without a word read, and the first line of a refusal says it in words, so a script, a person and another model read it alike.
| Exit | Type | What it means |
|---|---|---|
| 0 | none | done |
| 1 | OpeningWarning | every call came back with no answer, so nothing was read |
| 2 | UsageError | the command or the call asks for what cannot be done, and nothing was spent |
| 3 | StoppedWarning | the run stopped before its plan's end, so its records are partial; --kept finishes them |
| 4 | FrameError | the frame is refused |
| 5 | RecordsError | records or a report are not this frame's run |
| 6 | BatteryError | the battery cannot be made, set up or reached |
| 7 | FileNotFoundError, FileExistsError, PermissionError, IsADirectoryError, NotADirectoryError | a file or a folder cannot be read or written |
| 70 | any other | a bug in Gadfly, which prints its traceback |
| 130 | KeyboardInterrupt | stopped by Ctrl-C |
| 141 | none | the program reading its output stopped reading (| head), so the command ended quietly, as any Unix tool does |
UsageError
The command or the call asks for what cannot be done, and nothing was spent. When Gadfly prints what to do, it opens the second column.
| You see | What to do |
|---|---|
UsageError: the plan needs 40 calls; the budget allows 10 | gadfly plan shows the calls a whole run needs. --calls is below the plan, so no file was made. |
UsageError: unrecognized arguments --stop | gadfly run --help shows its arguments. Flags are named in full, since a shortened one would change meaning when a longer one is added. |
UsageError: --battery is module:Class (for example midwife.batteries.egemma:EGemma) | Name the class after a colon. |
UsageError: the plan has no trial 'controll' | gadfly plan lists its trials. Name one of them with --only. |
UsageError: battery 'egemma' does not take calls at once | Leave out --parallel, as this battery takes one call at a time. |
UsageError: - reads standard input, which is a terminal | pipe the file in, or name it. |
UsageError: standard input is read once | name - for one file at most. |
UsageError: --json and --out - both print to standard output | choose a file for --out. |
UsageError: the token budget (0) is spent | --token-budget must cover one call's --output-tokens. Nothing was spent and no file made. |
FrameError
The frame is refused as written, and Gadfly never repairs it. gadfly check-frame prints each refusal as a FrameError and where it is, without a call, and Write a frame lists the common ones.
| You see | What to do |
|---|---|
FrameError: file 'frame.json' is not JSON at line 2, column 1 (Expecting value) | Correct the file at that place; a frame is plain JSON. |
FrameError: file 'frame.json' is not UTF-8 text | Save the file as UTF-8. |
FrameError: the body has no span 'r9' | anchors name spans of the body. Name as anchors only the ids of its spans. |
FrameError: required influence is tested by ablation, and none is declared | set ablation to true. |
RecordsError
The records, or a report, are not what this frame's run made, so no verdict is read from them.
| You see | What to do |
|---|---|
RecordsError: a record belongs to another frame | The records were made under another frame: another file, or this one edited after the run, since any change makes a new hash. Read them with the frame they were made under. |
RecordsError: a record of trial 'baseline' was made on another body | The trial's text differs from the one the record was made on. The frame, or the way Gadfly plans it, changed after the run. |
RecordsError: a record is a JSON object or RecordsError: a record's trial is not a str | A line of the records file is not a record Gadfly wrote: another file, or one edited by hand. |
RecordsError: file 'records.jsonl' is not JSON at line 3, column 1 (Expecting value) | That line is not a record; the file was cut short or edited. |
RecordsError: a record's usage counts are not whole numbers | A run keeps usage as whole counts by name, so something else wrote this record. |
RecordsError: a record's served is not a str | The same for what the battery said it served. |
RecordsError: file 'records.jsonl' is not UTF-8 text | The file was written by another tool or edited in another encoding. Save it as UTF-8. |
RecordsError: two records for input 'pr', trial 'baseline', run 2 | The same call is kept twice, usually from two files joined into one. Keep one of them. |
RecordsError: a kept record of trial 'baseline' was made from another prompt | A record passed as --kept was made with a prompt this frame does not plan for that call. Resume only from the records this frame's own run made. |
RecordsError: file 'kept.jsonl' holds a lone surrogate at line 3 | no file can hold it; correct or drop that record, then run again. That record holds text, often from another tool, that cannot be written back, and nothing was spent. |
RecordsError: a portfolio reads `gadfly report --json` documents, and this is not one | Each file portfolio reads is the output of gadfly report --json. |
BatteryError
The battery cannot be made, cannot serve this frame as it is set up, or cannot reach what it needs, and no call was made.
| You see | What to do |
|---|---|
BatteryError: no eGemma key | name a file that holds it in the environment variable EGEMMA_API_KEY_FILE, or set EGEMMA_API_KEY. |
BatteryError: file '/Users/you/.egemma/key' is empty | the environment variable EGEMMA_API_KEY_FILE names it; write eGemma's key into it. |
BatteryError: the router takes no data policy | update the router, so that each call reaches only endpoints that keep no data. The unified battery sends each call under the router's deny data policy, and the router's GET /health does not list it. |
BatteryError: the router at 'http://127.0.0.1:18766/health' could not be asked what it takes | start the router, or install it. The unified battery asks the router, before any call, whether it takes the deny data policy. |
BatteryError: the router takes no endpoint pins | update the router, or leave battery.endpoints out. The unified battery pins a frame's endpoints through the router's own header, and the router's GET /health does not list it. |
BatteryError: battery.endpoints lists 1 to 16 endpoint tags | each a tag the router knows, with no comma or space around it. |
BatteryError: no router token | set the environment variable CODEX_ROUTER_TOKEN, or install the router. The unified battery found no token in the variable or in the router's own settings file. |
BatteryError: this frame asks for battery 'egemma', not 'unified' | A frame names its battery in battery.kind, and another battery is another frame. Pass the one it names. |
BatteryError: 'midwife.frame:parse' is not a battery | a battery has a name, check() and observe(). The name after the colon is found, but it is not a battery class or object. |
BatteryError: module 'tests.fixtures.refund' has no 'NoSuchReader' | The module was found, but not the class named after the colon. Check its spelling and case. |
BatteryError: module 'mine' could not be imported | it said "No module named 'mine'". The battery's module is imported from the current directory. Run from the folder that holds it, or check its name. |
BatteryError: the evaluator's address must not hold a user or password | A battery's address names a user or a password. Gadfly refuses it without repeating it; take the credentials out of the address. |
BatteryError: the evaluator's address 'http://127.0.0.1:99999' has no valid port | a port is a number from 0 to 65535. |
BatteryError: battery.timeout is a number of seconds, up to 3600 | The local-json battery's whole-call deadline is missing a positive number, or asks for more than an hour. |
BatteryError: battery.reasoning is true or false | The egemma battery reads "reasoning": true to let Gemma think first; Gemma has no levels. |
BatteryError: battery.reasoning is one of none, minimal, low, medium, high, xhigh, max | The unified battery passes battery.reasoning to the model as an effort, one of these words. |
File errors
A file or a folder the command names cannot be read or written, and nothing was spent. These are Python's own errors, said in plain words.
| You see | What to do |
|---|---|
FileNotFoundError: file 'frame.json' does not exist | Check the path, or the file that the environment variable EGEMMA_API_KEY_FILE names. |
FileNotFoundError: folder 'runs' does not exist | make it, or choose another --out. |
PermissionError: folder 'runs' cannot be written | choose another --out. The folder is not yours to write. |
IsADirectoryError: 'runs' is a folder, not a file | Name the file in the folder, not the folder. |
NotADirectoryError: path 'notes.txt/frame.json' runs through a file, not a folder | A part of the path that should be a folder is a file. Check the path. |
FileExistsError: file 'records.jsonl' exists already | kept records are never written over; choose a new --out, or finish it with --kept. To finish a stopped run, pass its file as --kept with a new --out. |
KeyboardInterrupt
Ctrl-C stops a run, and every record made so far is already in its file.
| You see | What to do |
|---|---|
KeyboardInterrupt: stopped by Ctrl-C after 12 calls; they are in file 'records.jsonl' | finish with --kept 'records.jsonl' and a new --out. Only the missing calls are made, and the new file holds the whole run, to report as one. A crash leaves the file the same way. |
OpeningWarning
Calls came back with no answer, as openings: each is kept, and reads as no answer. The run goes on, and run exits with 1 only when every call did.
| You see | What to do |
|---|---|
OpeningWarning: 10 of 10 calls came back as openings, most often 'eGemma could not be reached (ConnectionRefusedError)' (10 times) | nothing was read; run again into a new file once it answers. Nothing answers at EGEMMA_URL, by default http://127.0.0.1:8000: start eGemma, or point the variable at it. |
"the router could not be reached (...)" or "the evaluator could not be reached (...)" | The same for the router, at http://127.0.0.1:18766, or for your evaluator, at the frame's battery.url. |
"eGemma answered 401" or "the router answered 401" | The key or the token is wrong. |
"eGemma answered 503 after 3 retries" | eGemma stayed busy or has no model loaded; the battery waits and retries before it gives up. Check eGemma's own log. |
"the router refused model 'ultra-spark/deepseek-v4.1-flash-1017', whose provider trains on its inputs" | The router's own list says this model's provider trains on its inputs, and the call went under the deny data policy. Choose another model, or name this one under trainsOnInputs to send it there. |
"no endpoint that keeps no data serves this model (the router's provider setting); naming it under trainsOnInputs allows one that may train" | No endpoint the router may use for that model keeps no data, and the frame does not consent to one that trains on its inputs. Choose another model, or pin endpoints with battery.endpoints. |
"a cap of 64 output tokens leaves no room for a thought beside the answer's 64" | With "reasoning": true, the egemma battery keeps 64 of each call's output tokens for the answer. Raise --output-tokens, to 4160 for a whole thought; no request was made. |
"eGemma answered 422" | eGemma refused the request as written. With "reasoning": true, an eGemma older than its reasoning change does not know the field: update eGemma. |
StoppedWarning
The run stopped before its plan's end, so its records are partial, and run exits with 3. The records made are kept, and --kept finishes them.
| You see | What to do |
|---|---|
StoppedWarning: the run stopped after 3 of 8 calls; the token budget (30) is spent | finish with --kept 'records.jsonl' and a new --out. Raise --token-budget first. |
StoppedWarning: the run stopped after 1 of 8 calls; the battery reported 900 output tokens for a call capped at 64 | finish with --kept 'records.jsonl' and a new --out. The battery spent more output tokens on one call than --output-tokens allows, and that call's record is kept. A battery that writes before it answers spends tokens on the whole call: set --output-tokens to what a whole call may spend. |
Reading a verdict
A verdict that reads unresolved is a finding, not an error, and report exits with 0.
| You see | What it means, and what to do |
|---|---|
unresolved, with no agreed level in ... | Fewer than agree answers matched in those trials: the reader split, or the calls came back as openings. The run's OpeningWarning says which. |
unresolved, with only open checks beside it | Every trial agreed, but a check the category needs is still open. Reading a verdict explains carriage and each check. |
Developing Gadfly
| You see | What it means, and what to do |
|---|---|
1 transcript(s), copies or references differ | A page quotes output the code no longer prints, or a copy, frame view or API section is stale. Run uv run python tools/doc_outputs.py --write. |
tests/test_kept_runs.py fails | A change to the verdict rules changed a kept run's category. That is a method change: say so in the docs, and never edit the kept runs. |
npm run docs:build fails on a dead link | A link names a page that does not exist. test_every_link_lands finds it, with any heading anchor that does not. |