Skip to content

io-cli 0.22.0 - #58

Merged
aakashpawar1999 merged 8 commits into
mainfrom
develop
Aug 27, 2026
Merged

aakashpawar1999 merged 8 commits into
mainfrom
develop

Conversation

@aakashpawar1999

Copy link
Copy Markdown
Contributor

Release merge for 0.22.0. Everything in this PR arrived on develop through #57, which was 8/8 green.

What a turn cost, and whether it worked. /cost and /stats read rows io-harness has written since its 0.18.0 and no io-cli release had ever opened. /usage becomes an alias for /cost. Prices are not compiled in — the table is filled from the catalogue the operator's own provider serves, dated, and refreshed from /config with every changed rate shown before a byte lands.

Folded in: crossterm 0.29, the four defects 0.21.0 recorded and did not fix, and two status-line priority inversions.

Gate Result
cargo test --lib --tests 83 binaries / 1,165 passed / 0 failed / 24 ignored
cargo test --test live -- --ignored 24 passed / 0 failed, real provider, 808s
fmt, clippy, nightly rustdoc all exit 0 (clippy refused 3, rustdoc refused 5)
CI on #57 8/8

live_f3_f4_plan_switches_and_the_status_line_says_so passes for the first time since 0.14.0 — the failure the 0.21.0 record named as the first thing this release should look at.

The adversarial pre-merge review returned 23 findings behind a green suite; the top one meant no operator would ever have received prices at all.

ratatui chooses the crossterm it re-exports by feature, so the direct crate and
`crossterm_0_29` move in one commit or the build carries two crossterm versions
whose key events are different types. That is the whole reason 0.18.0 pinned this
at 0.28 and added the dependabot ignore, and both notes said the same thing: the
bump is a deliberate change of the flag and the entry together, not a bot's to
make.

crossterm 0.29's only breaking change is a `KeyModifiers` Display fix, and no
source file in this crate formats one. `Cargo.lock` resolves a single crossterm.

The `ignore` list is now empty and gone with it. Nothing replaces it: one
crossterm in the tree is the two lines in `Cargo.toml` agreeing, and a future bot
PR that moves the direct crate alone fails to build rather than passing quietly.
All four were real, and one of them is a precondition for this release rather
than a tidy-up: a price row is addressed as `prices.models."gpt-4.1"`, and
`edit::split_path` split a dotted path on `.` with no notion of a quoted
segment while `regions` stripped quotes after the same naive split. The two
sides normalised differently, so any key containing a dot reached the wrong
place or no place — a duplicate table, caught only by the read-back, reported
to the operator as "the edit would have produced a file that does not parse"
for a perfectly legal TOML key. A dot in a key can only be spelled quoted, so
this was every model id with a version number in it.

One quote-aware splitter now serves both sides, decoding through `toml` rather
than by hand: a basic string takes the full escape set, a literal string takes
none, and whitespace around a dot is legal. The two sites that put a segment
back into a document re-spell it through the inverse. `providers` and `servers`
are unaffected and were checked rather than assumed — both address entries by
numeric index and put names in the entry body, already escaped.

The other three:

Removing an entry spliced to the next header's first byte, so it carried away
the comment block documenting the *next* section. `move_entry` did the same.
Both now stop at the trailing comment run.

`move_entry` into the last position of a file with no trailing newline
concatenated the header onto the final value — usually a parse refusal, but
silent corruption when the file ends on a comment, which swallows the header
and lands the moved entry's keys inside the previous table. It now guards the
newline in both directions, as `Kind::Append` already did.

`~/.claude/skills/CONVENTIONS.md` is a legal loose skill file that
`skill_files` itself produces, and `import::plan` matched it on basename and
planned it as *instructions* — appending a skill's whole body into the standing
instructions loaded on every turn, forever. The skills-root guard now runs
ahead of the name match. The comment asserting that arm was "exactly the skill
files" was false in the direction that loses skills, and is corrected with it.

And a codex `[mcp_servers."a.b"]` server was being dropped silently, because
`sections()` reported three segments where there were two. Fixed by the
splitter; the filter that looked wrong was starving, not wrong.
io-harness has recorded a row per provider call for four releases — the model,
the whole token split, the latency, the time to first token — and io-cli read
none of them, while `src/status.rs` explained in a doc comment that a figure
with a currency in front of it "would be one this interface invented". That
sentence was true when it was written and stopped being true in io-harness
0.18.0.

`/cost` commits what has been spent: this run, this session, by model and by
day, with cache reads and writes shown as the breakdown of the prompt they are
rather than added on top of it. `/stats` commits how the work has gone:
outcomes, days, the first-try counts with their denominator named, gate
failures by phase with the two vocabularies kept apart, the slowest calls, time
to first token, and what the store holds. `/usage` is an alias for `/cost` and
earns no row; it answered `/status` before, under an argument that a page whose
whole subject is spending supersedes.

Prices are not compiled in. `verify::catalogue` has fetched the model catalogue
since 0.1.0 and mapped every row down to its id, discarding the price on it;
`src/prices.rs` keeps it, filtered to what the connected provider serves and
spelled the way that provider names it. An operator with no provider connected
has no prices, sees tokens and no currency, and nothing claims a cost. For
three of the four providers the rates necessarily come from the reference
catalogue rather than the vendor — OpenAI, Anthropic and Google publish none —
and the page says which.

Three honesty rules are held as hard as the arithmetic: a call the provider
reported no usage for is unknown and never free, a model with no rate makes the
total a floor and the page renders that count rather than hiding it, and every
surface that draws money names the catalogue and the date it was read.

Two status-line defects are fixed because this release would have made both
worse. The footer fitted its right-hand group all-or-nothing, so a counts row
that grew past the line took the policy layer, the containment mode and the
planning phase off the screen together — the failure the footer's own comment
says it exists to prevent, arriving from the other side, and the reason the
live planning capture has failed since 0.14.0 added `ctx N%`. Counters now
yield. And `fields` pushed `planning` to the right of the counters, so the
narrow line dropped it before the token count, inverting the priority written
five lines above it. This release adds another counter to that row.

`Inspect` is re-split to hold the two new commands. `/image`, `/copy` and
`/copy diff` move to `Turn`, where they belonged — all three act on the turn
that just finished and none of them asks the store a question.

The fold that `status` and `context` each carried a copy of is now
`src/page.rs`, which is what both new pages are built from.
The table ships empty and nothing is compiled in. It is filled from the model
catalogue that provider serves — the fetch io-cli already made to offer a model
list, whose prices it discarded — written into io-harness's own `[prices]` as a
date and one line per model, and re-read from a row on `/config`.

The refresh shows every rate that would move before a byte lands, and the
reason is sharper than courtesy: the file records what a rate is and not where
it came from, so io-cli cannot tell a correction the operator typed from a
value an older catalogue served. It does not guess. It shows the lot and lets
the operator decline the lot, which is the shape `/import` established.

A refetch that comes back far shorter than the table it would replace is
refused and the old table kept. A truncated read that silently turned most of
an operator's spending into "unpriced" would read on `/cost` as a bill that
suddenly fell, which is the one failure in this area that loses money quietly.

`[prices]` is io-harness's section and `deny_unknown_fields`, so a key of
io-cli's own beside `as_of` and `models` would not be ignored — it would make
the whole configuration file unreadable. Everything io-cli needs to remember
that io-harness does not model lives under `[app.io-cli.prices]`: which
catalogue to read, what the last read was in words, and how many models it
priced.

The clock is read in the driver because the driver is the only place
`tests/timing.rs` permits it; the conversion to a date is a pure function of a
number, which is also what makes it testable.

Two of the new tests were failing for reasons that were not production bugs and
are fixed as tests: the span helper's common-suffix scan swallowed a shared
`.0`, and a test asserting a server arrives cannot use the variable another test
deliberately leaves unset — an unresolvable `${env:}` is refused and rolled
back, correctly.
Twenty-three findings, and the first one meant no operator would ever have got
prices at all.

**The first fill could not be written.** Both write paths pushed two bare
`Edit::set`s into `[app.io-cli.prices]`, a section no file io-cli has ever
written carries. Every edit in a batch resolves against the document as it was
before the batch, so both fell to the append arm, each emitted its own header,
and the read-back refused the lot — the exact defect `Edit::section` was added
to fix, one section over, on the two edits the driver adds itself. It survived
that fix because nothing under `tests/` can link the driver, so the bookkeeping
edits now live in `prices` where a test can reach them.

**`/clear` kept the previous conversation's money on the footer.** `forget_run`
cleared every other run-scoped field and not `cost`, so a cleared session drew a
currency figure beside a blank token count.

**`source_url` could not work.** `named`'s catch-all discards every row for a
`compatible` endpoint, so io-cli fetched the operator's own catalogue and threw
it away — while `settings.rs` and the README both called that key the only way a
self-hosted operator gets prices. A named catalogue is not narrowed: the
operator has already answered the question the filter exists to ask.

**A filled table would have turned `/config` into a thousand-row picker.**
io-harness records leaf keys, so every model contributed two to five origins,
and the `CATALOGUE` comment saying `[prices.models]` is deliberately not a row
was being undone by the sweep below it.

The rest: `note_cost` ran on every event under a comment saying it ran on steps;
the footer's new trim popped the interrupt hint before any counter, so a full
row lost `esc stops` exactly when a turn was running; an unrecorded model count
rendered as a measured `0`; a failed catalogue read reported "no rate has moved",
a positive claim about a read that did not happen; `too_short` compared against
io-cli's own record rather than the file, so it was inert in every case it
exists for; a real cost under a hundredth of a cent rendered `$0.0000`; the
floor caveat pointed at a cost row that is suppressed when nothing is priced;
"unpriced" meant two different things on one page; and a `[prices.models."<id>"]`
sub-table — legal TOML io-harness reads — is now refused by name instead of
producing a duplicate-key error.

Three test defects went with them: an assertion pinning the `models: 0` bug, a
`contains("0 ms")` that is satisfied by `300 ms`, and a width sweep whose floor
assumed the key hint was still droppable.

83 binaries, 1,164 passed, 0 failed, 24 ignored.
The nightly rustdoc gate is not in `ci.yml`, so it is run by hand at the seal —
and this is the fifth release in six where it has found something no other gate
can see. Four of the five are this release's: a doc link to `PriceTable` with no
path, a public `spell` linking to the private `segments` it is the inverse of,
and two `[`array`]` links that are ambiguous against the primitive type. The
fifth predates it — `crate::attach::unquote` has never resolved — and is
corrected here rather than carried.

Clippy: a same-type cast in the date arithmetic, a manual checked division of
the first-try share, and a `sort_by` that says `sort_by_key` more clearly.
`app.io-cli.prices.source_url` is documented as the only way an operator on a
self-hosted or compatible endpoint gets prices, and the only test either path
had was the one asserting the empty result the defect produced. This is its
companion: the same endpoint with a catalogue the operator named, taken whole,
attributed to its URL rather than to the vendor, and carried through
`edit::apply` into a `PriceTable` that answers.
io-cli 0.22.0 — what a turn cost, and whether it worked
@aakashpawar1999
aakashpawar1999 merged commit 7290438 into main Aug 27, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant