I was recording usage and cost for AI coding sessions when I noticed something odd: one session had no cost entry at all.
My first assumption was that the measurement tool had failed. But when I ran agent-cost directly against the same Claude Code session, it completed successfully in 36.454 seconds. Through the spec-lane adapter, however, the same measurement failed after 30.019 seconds.
The reason was simple: the caller had a 30-second timeout.
What mattered more than the timeout itself was what happened afterward. The failed measurement did not produce a $0 record. It produced no measurement record at all.
That distinction became the main lesson from this incident:
Measurement failed != $0
Failing to measure something is not the same as measuring it as zero.
The same session succeeded directly and failed through the adapter
I first reran the session from Issue #42 directly with agent-cost:
agent-cost measure \
--format json \
--session-id <session-id>
The result:
exit: 0
wall: 36.454s
JSON parse: success
protocol: measure/v1
The measurement took longer than 30 seconds, but it completed normally.
I then passed the same session through an AgentCostTelemetryAdapter built from the pre-fix Git revision cca0338....
30.019s
TelemetryImportFailed
Command failed
This time it stopped around the 30-second mark.
For comparison:
| Path | Result | Time |
|---|---|---|
Direct agent-cost measure
|
Success | 36.454s |
| Pre-fix adapter | Timeout | 30.019s |
| Post-fix adapter | Success | 35.410s |
The pre-fix adapter had this default:
this.timeoutMs = opts.timeoutMs ?? 30_000;
So the underlying measurement could finish in roughly 36 seconds, while the caller terminated it after 30.
There was another problem: the error message was only Command failed. It did not say that a timeout had occurred or that the process had been stopped after 30 seconds.
From that message alone, it was difficult to distinguish a failure inside agent-cost from a timeout imposed by its caller.
No record is not a zero-dollar record
I wanted to know what a failed measurement left behind.
For this part, I used a separate controlled reproduction with a deliberately short timeout. This was not the same real session used in the timing comparison above.
When lane calibrate failed during measurement, the result was:
CalibrationObservation: 0
cost_ledger: 0
It did not write a record such as:
{
"cost_usd": 0
}
There was simply no record.
The source code matched that behavior. If adapter.measure() fails, the command returns before generating the observation and before writing the cost ledger.
Running emit-metrics against the empty ledger produced:
built 0 record(s), coverage.status=no_data
Here, no_data does not mean "the measured cost was zero." It means there is no measured record available.
That matters once the numbers are aggregated. If ten failed measurements in a hundred tasks are silently converted to $0, the resulting average can look lower than the evidence actually supports.
Zero is a value. Missing measurement evidence is a different state.
The fix was more than changing 30 seconds to 180
PR #43 changed the default timeout from 30 seconds to 180 seconds:
DEFAULT_AGENT_COST_TIMEOUT_MS = 180_000
It also added a CLI override:
--agent-cost-timeout-ms <n>
The option is available for four commands:
calibrateemit-metricsnextusage-import
The accepted range is from 1ms through 3_600_000ms.
The timeout error was also made explicit. For example, a controlled 200ms timeout now reports:
timed out after 200 ms (killed with SIGTERM)
That makes it much easier to tell the difference between a measurement command failing on its own and the caller terminating it because of its deadline.
There is still a boundary here: 180 seconds is not a strict wall-clock guarantee. The implementation sends SIGTERM when the timeout is reached. A child process that does not terminate immediately after that signal can continue beyond the nominal timeout.
After the fix, a 38-second run persisted the measurement
Increasing the timeout is useful only if the measurement can actually finish and its evidence is saved.
I reran another historical session and lane snapshot using the v0.10.1 CLI with the default 180-second timeout.
exit: 0
wall: 38.01s
This time the process did more than return successfully:
CalibrationObservation: 1
cost_ledger: scope=lane
emit-metrics: coverage.status=complete
The observation was created, the cost ledger was written, and metrics coverage reached complete.
There is also a historical PR record reporting a successful run of roughly 39 seconds. I treat that separately: the raw log containing the exact historical wall time is no longer available, so that reported value is not the same evidence as the independently reproduced 38.01-second run.
Completing a measurement does not prove the number is correct
Another boundary became important while checking the fix.
What I verified here is that the measurement process can complete, return a protocol payload, and persist its result.
That does not prove all of the following:
- that the estimated dollar amount is correct
- that the pricing catalog is correct
- that a session has been attributed to the correct task
In fact, the historical record and the new reproduction produced different token counts and estimated costs. They also used different agent-cost versions.
I did not investigate the cause of that difference as part of this timeout work.
Measurement completion, cost accuracy, and task attribution are separate claims. A successful run only establishes the first one.
exit 0 can still mean measurement incomplete
There was one more edge case worth keeping.
calibrate exits with code 2 when measurement fails. usage-import, however, has a different contract.
In some measurement-failure cases it records:
measurement-incomplete
matched: false
and writes no ledger entry, while still returning exit 0.
So this is not a safe equivalence either:
exit 0 == measurement succeeded
The exit code, measurement state, and persisted evidence need to be interpreted separately.
Don't collapse unknown into zero
The immediate bug was a 30-second timeout. The more useful lesson for me was about state representation.
For measurement systems, these are not interchangeable:
0
unknown
failed
not measured
Collapsing all of them into zero makes aggregation easier. It also destroys information about what actually happened.
This applies beyond AI coding cost: test results, evaluation scores, usage metrics, and observability pipelines can all run into the same problem.
The rule I kept from Issue #42 is still the simplest description:
Measurement failed != $0
As more of an engineering workflow becomes automated, preserving the difference between "the value was zero" and "we never obtained a value" becomes more important, not less.
References
- Issue #42 — agent-cost timeout
- PR #43 — default 180s / timeout override / diagnostic
- spec-lane v0.10.1
A note on the version reported in the original issue
Issue #42 records the historical runtime as:
lane 0.9.1, cca0338 on main
In Git, however, cca0338... is the peeled commit for the v0.10.0 tag, and packages/cli/package.json at that revision reports version 0.10.0.
The 30.019-second reproduction in this article was built from the cca0338... source revision.
I have not established why the historical executable reported 0.9.1, so I do not claim that the original incident occurred on the v0.10.0 CLI.
Top comments (0)