Repository navigation
io-cli 0.22.0 - #58
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release merge for 0.22.0. Everything in this PR arrived on
developthrough #57, which was 8/8 green.What a turn cost, and whether it worked.
/costand/statsread rows io-harness has written since its 0.18.0 and no io-cli release had ever opened./usagebecomes 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/configwith 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.
cargo test --lib --testscargo test --test live -- --ignoredlive_f3_f4_plan_switches_and_the_status_line_says_sopasses 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.