Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,75 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.27.0] - 2026-08-28

The store stops being something you cannot see, undo stops being
all-or-nothing, and the work stops ending when the terminal closes.

**`/store` says what the run store is holding, and three verbs change it.**
`~/.io-cli/runs.db` has held every session, run, step, event, provider call,
snapshot and restore point since 0.15.0, with no retention policy, no rotation
and no way to look at it. The page reports the file's own page arithmetic —
what it costs on disk, what is already free inside it, and what each session in
it holds. `/store rm <id>` removes one session, `/store sweep <date>` removes
every session created before a timestamp, and `/store compact` returns the free
pages to the filesystem. Each descends into a confirmation whose first row is
"leave it", which is the row the cursor starts on.

**A deletion does not shrink the file, and io says so.** SQLite frees pages into
the database rather than out of it, so a removal moves bytes into the free space
*inside* the file and the file on disk stays the size it was. A `VACUUM` is the
only reclamation available, because every store this product has created was made
without `auto_vacuum` — so `/store compact` is a thing you ask for, it needs
roughly the file's own size in free disk space while it runs, and it reports the
bytes the file actually shrank by rather than an inference from the freelist.

A sweep refuses a session that still holds a resumable run, and names it. It asks
you to agree to the rule rather than to a count, because io-harness exposes no
reader for the column the sweep filters on and the nearest substitute would
under-state what is about to go — filed as io-harness#216. The figures are
reported the moment it finishes, refusals included.

**`/undo` is the size of the mistake.** `/undo <path>` puts one file back,
`/undo step <n>` reverse-applies one step's diff, and a bare `/undo` is the whole
turn — the same thing the rewind chord has always done, now reaching the same
implementation. One file has four possible answers and they read as four
different sentences: it came back, it was removed because the run created it, or
nothing changed — for either of two different reasons.

Undoing a step is order-sensitive: a later step still standing on the same lines
makes the revert stale, io-harness leaves the file alone rather than
fuzzy-matching it, and io says why. A restore does not know about an edit you
made after the turn, and the confirmation says that before you agree to it.

**`/export` writes the conversation as markdown, and `/export trace` writes one
run's canonical trace.** Both go into the workspace under the session's own path
policy, and an existing file is refused rather than overwritten. The trace is
written exactly as io-harness produced it — not parsed, not reserialised, not
pretty-printed — because being canonical is the whole of what it is for.

**Undoing a turn now announces itself.** `EventKind::Rewound` and
`EventKind::Reverted` are emitted only by the observed forms of io-harness's
rewind calls, and this product had called the plain ones since 0.4.0 — so neither
event had ever fired, and the code drawing one of them had been unreachable since
the day it was written.

**One event that reached nobody now has a line.** A read started before the model
had finished asking, and thrown away — work that was paid for and not used. It
draws only when something was discarded, because a line in every transcript
saying nothing went wrong is a line nobody reads. The other eight silences in
this interface were reviewed in the same pass and keep their routes, which are
better arguments than drawing them would be.

**`/contain` moves from *this turn* to *the session*.** It is a posture that
changes how every later turn is driven rather than something that acts on the
turn just finished, and moving it is what makes room for `/undo` without widening
the ten-command bound. `Inspect` reaches ten with `/store` and `/export`.

No configuration key is added, nothing is read at startup that was not read
before, and no model can reach any of the store operations.


## [0.26.0] - 2026-08-28

A turn asks for the model, the vendor and the amount of thinking the work
Expand Down Expand Up @@ -2191,6 +2260,7 @@ client, tool, sandbox, policy engine or session store of its own.
- There is no crates.io publish and `cargo install` is not an install path.
- No test in this release asserts on wall-clock time.

[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.26.0...HEAD
[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.27.0...HEAD
[0.27.0]: https://github.com/initorigin/io-cli/releases/tag/v0.27.0
[0.1.1]: https://github.com/initorigin/io-cli/releases/tag/v0.1.1
[0.1.0]: https://github.com/initorigin/io-cli/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "io-cli"
version = "0.26.0"
version = "0.27.0"
edition = "2021"
# io-harness declares 1.95, set by `libsqlite3-sys` calling the std `cfg_select!`
# macro from its build script. Nothing here can be lower than what the harness
Expand Down
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ above a row that ranked there for reasons having nothing to do with it.
| --- | --- |
| `/model` | change the model the next turn is sent to |
| `/effort` | how much reasoning the next turn buys: low, medium, high, or off |
| `/undo` | put work back: `<path>` for one file, `step <n>` for one step, bare for the run |
| `/contain` | run turns contained, so the agent can fan out: on, off, or ask |
| `/plan` | make turns propose a plan before they work: on, off, or ask |
| `/steer` | send what is queued into the turn that is already running |
Expand All @@ -529,6 +530,8 @@ above a row that ranked there for reasons having nothing to do with it.
| `/skills` | every skill, shipped or yours: what it is for, whether it is on, and its file |
| `/cost` | commit what this run, this session and this install have spent |
| `/stats` | commit how the runs have gone: outcomes, first-try, gates, latency |
| `/store` | commit what the run store holds; `rm <id>`, `sweep <date>` and `compact` change it |
| `/export` | write this conversation as markdown, or `trace` for a run's canonical trace |

**configure**

Expand Down Expand Up @@ -2021,6 +2024,126 @@ browser at all. io-cli's own `[app.io-cli.browser]` is read from either scope.
locale behind `glyphs`. See [Reading it without seeing
it](#reading-it-without-seeing-it).

## What the store is holding

**`/store` commits a page: what the run store costs on disk, what is already free
inside it, and what each session in it holds.** Everything on that page is
io-harness's own arithmetic — `page_size × page_count` for the file, and
`page_size × freelist_count` for the free part — read from the store rather than
computed here.

The distinction between those two numbers is the whole reason this page exists.

**A deletion does not shrink the file.** SQLite frees pages *into* the database
rather than out of it, so removing a session moves bytes from the file's size
into the free space inside it and the file on disk stays exactly the size it was.
Every store this product has ever created was made without `auto_vacuum`, so
there is no incremental reclamation either: a `VACUUM` is the only thing that
returns the space, and it is `/store compact`.

That matters because `~/.io-cli/runs.db` has held every session, run, step,
event, provider call, snapshot and restore point since 0.15.0, with no retention
policy and no rotation. Until this release there was no way to look at it, and no
way to shrink it.

Three verbs change it, and each one shows what it will do before it does it:

| | |
|---|---|
| `/store rm <id>` | remove one session and everything keyed to it |
| `/store sweep <date>` | remove every session created before that timestamp |
| `/store compact` | rewrite the database without its free pages |

**Each descends into a confirmation whose first row is "leave it".** That is the
row the cursor starts on, so the keystroke you give by reflex is the one that
changes nothing.

**A removal is final and takes the session's restore points with it.** Its
*memory* stays — the agent's durable notes belong to the workspace rather than to
the session, so removing a session unlearns nothing — but the rewind for those
turns is gone, and that is the part that bites later.

**A sweep refuses a session that still holds a resumable run**, and names it. A
date is a policy applied to sessions nobody looked at, and a crash-resumable tree
that vanished because it was old would be the worst thing this command could do.

**The sweep asks you to agree to the rule rather than to a count**, and the
reason is a gap in io-harness rather than a choice: `sweep_sessions` filters on
`sessions.created_at` and nothing exposes that column, so the set a date selects
cannot be counted until the sweep has run. The nearest substitute — a session's
first turn — is always *later* than the session itself, so a count built on it
would under-state what is about to be deleted. The figures are reported the
moment it finishes, refusals included. Filed as io-harness#216.

**Compaction is not free while it runs.** It rewrites the whole database and
needs roughly the file's own size in free disk space to do it, so it is a thing
you ask for rather than something a deletion does on your behalf. It reports the
bytes the file actually shrank by, measured, not inferred.

Nothing here happens on its own — there is no retention setting, no threshold and
no sweep at startup — and no model can reach any of it.

## Putting work back

**`/undo` is the size of the mistake.** Until this release the only instrument was
the rewind chord, which undoes a whole turn, and that is the wrong size for *this
one file went wrong*.

| | |
|---|---|
| `/undo <path>` | put one file back as it was before the run |
| `/undo step <n>` | reverse-apply what one step wrote |
| `/undo` | the whole turn — the same thing the rewind chord does |

**One file has four possible answers and they are four different sentences.** It
came back; it was *removed*, because the run had created it; nothing changed
because the previous contents were not kept; nothing changed because this run
never wrote that path. The last two both mean your file is untouched, and they
mean it for different reasons.

**Undoing a step is order-sensitive.** Reverse-applying one step's diff while a
later step's change still sits on the same lines finds context that has moved,
and io-harness leaves the file alone rather than fuzzy-matching it into
something nobody wrote. Undo the newest step first and it applies — and io says
so when it happens, because "nothing changed" without that sentence reads as a
bug.

**A restore does not know about an edit you made afterwards.** The file comes
back from the snapshot taken before the run first wrote it, and that snapshot is
not compared against what is on disk now, so a change you made by hand after the
turn is overwritten. The confirmation says so before you agree to it.

Every restore goes through the same path policy a write does, and a *removal*
asks the policy separately and refuses anything that is not an outright allow.

## Taking the work out

**`/export` writes this conversation as markdown, and `/export trace` writes one
run's canonical trace.** For the review that happens in a pull request, or a text
editor, or a message to somebody who was not there.

| | |
|---|---|
| `/export` | the conversation, as markdown |
| `/export <path>` | the same, where you say |
| `/export trace` | the last run's canonical trace |
| `/export trace <path>` | the same, where you say |

Both are written into the workspace under the session's own path policy, and
**an existing file is refused rather than overwritten** — an export is a
snapshot, and the next one you take is a different snapshot.

**The trace is written exactly as io-harness produced it.** Its whole value is
that it is canonical: io-harness leaves wall-clock stamps, measured durations, an
ephemeral sandbox path and autoincrement ids out of it so that two runs of one
case can be compared. A trace io-cli reformatted would compare against nothing,
so io-cli does not parse it, reserialise it, or pretty-print it. It is
pipe-delimited text rather than JSON, and it takes a `.txt` extension for that
reason.

A turn that never finished is written as a turn that never finished, rather than
as one the agent had nothing to say to.

## What this release is not

The configuration file has reached your terminal since 0.14.0: every section of
Expand Down
Loading