Skip to content

feat(workbench): UI v1.1 — the object desk, fences, and detachable consoles - #354

Merged
paulocorcino merged 316 commits into
mainfrom
feat/ui-v1.1
Jul 28, 2026
Merged

feat(workbench): UI v1.1 — the object desk, fences, and detachable consoles#354
paulocorcino merged 316 commits into
mainfrom
feat/ui-v1.1

Conversation

@paulocorcino

Copy link
Copy Markdown
Owner

Summary

Brings the feat/ui-v1.1 line onto main: the workbench revamp track (PRD #185) through the object desk, the fence model, and detachable fences on a second monitor (PRD #344).

Highlights:

Verification

Local green gate on the tip commit: cargo fmt --check, cargo clippy -- -D warnings, cargo nextest run --workspace. CI runs the same on Windows and Linux.

🤖 Generated with Claude Code

paulocorcino and others added 30 commits July 25, 2026 05:47
…307)

Self-review fixes: the badge renders `—` (never a quiet 0) when the read fails, with the reason in its title; the fold refuses a non-ok reply; new coverage for the anchored .ralphy/ filter, an unmerged path, the rename output line, and the daemon Query arm's reply field; CONTEXT.md gains the change-set term.
min/vs minus language/**, nls/lang/**, *.d.ts, *.map and the four
language workers under assets/. 113 files, 5,570,129 bytes.
RED until the swap lands: 27 vendor/codemirror/ paths are still embedded.
Adds wb-monaco.js — the memoised AMD boot, the ADR-0035 "wb" theme, the
.toml→ini registration and the four disabled LSP mode configurations.
The source pane and the markdown raw-source editor both mount through
WBMonaco. Editor creation is now async (AMD loader), so rec.content stays
authoritative until rec.ed exists and a tab closed mid-boot mounts nothing.
CodeMirror 5, its 27 vendored files, cmMode() and the .cm-s-wb theme are
deleted; the app.js keyboard guard now looks for .monaco-editor.
…308)

15/15 over a real daemon on a scratch RALPHY_DAEMON_DIR: rust/ini language
resolution from the path, >=3 theme colour slots (mtk indices only — bracket
decorations ride on mtk1 and would fake the count), the find widget, a Save
round-tripped byte-equal to disk, CodeMirror 404, and the ADR-0035 ground.
Two HIGH defects from the independent review:
- automaticLayout was off, and nothing relayouts on resize — Monaco sizes
  from inline dimensions (CodeMirror reflowed from CSS), so a sidebar
  collapse or window resize clipped the pane until the next tab switch, and
  the detached popup, which has no tab bar, never relayouted at all.
- `tokens: false` on jsonDefaults killed JSON highlighting outright: that
  provider IS the json tokenizer and json has no basic-languages grammar to
  fall back on. Proved: with it off a .json tab has exactly 1 colour slot.

Plus the boot-gap lifecycle: liveness is now record identity, not `map.has`
(tab ids are stable per file, so close-then-reopen mid-boot could mount an
undisposable editor on a detached container); a create()/wiring throw no
longer masquerades as a boot failure; the read-only fallback pane tracks
applyFresh; and re-entering mountEditor cannot stack panes or duplicate a
model URI. Test grew 15 -> 22 checks with the boot gap, model disposal, the
markdown raw editor and a .json regression guard, and the exit count is
pinned exactly.
MARKS table + marker() classify the six producer statuses (modified,
added, deleted, renamed, untracked, conflicted) to a mark/cls pair;
anything else falls back to ?/st-unknown. fold() now carries mark,
cls and a rename-aware title per entry.
Adds a per-slug open/entries model, a click-to-expand chevron on the
Changes header, and a row list (mark + path + rename arrow) driven
off the same changes.list snapshot the badge already counts. Section
stays collapsed by default; the list caps at 45% height so it never
displaces the file tree.
wb_changes_309.py: 6 scenarios over a real daemon — collapsed by
default, one click renders 5 rows in git order, per-status markers,
rename shows its origin, untracked files appear, the list scrolls
without hiding the tree, and the narrow-viewport render stays intact.
15/15 checks pass. Screenshot at docs/screenshots/309-changes-list-2026-07-25.png.
The collapsed section now DOES carry a toggle (the new chevron), so
the old "offers no toggle control" check is replaced with one
asserting the header carries exactly one .chg-chev.
the_changes_section_renders_a_status_marked_list source-text pins
index.html and wb-changes.js, mirroring the #308 Monaco pin - the
JS/HTML deliverable has no Rust build compiling it, so the workspace
suite would otherwise stay green after a deletion.
The nudge send lives inside the blocking wait task, so it fires on every
exit path of the run — including the shutdown and client-disconnect arms
that abandon the `select!` loop while the child keeps living.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…310)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ordering token on `loadChanges` (a nudge landing mid-read could commit a
stale count), optional-chained `WBChanges` on the push path, a Rust source
pin for the browser half (CI runs neither node nor playwright), a second
registry entry so the nudge's repo name is really proved, malformed-frame
cases for `shouldReload`, and four mechanism assertions in the browser
acceptance (no `changes.list` in the stale window, no socket reopen behind
the refresh, B's own run proved to land, the manual move bound to the click).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed run

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Absence at the revision is an outcome, not an error: a newly added file
must diff against emptiness. Binary and over-cap follow the working-tree
reader's rules so both halves of one diff agree on what text is.
The daemon's argv builder validates the path by shape and stays pure; this
process is the one standing in the repo, so it refuses an escaping path
before any read. The JSON reply is a struct, not `json!` — serde keeps
declaration order, `json!` sorts keys.
The argv builder stays pure: the path is validated by shape only, so a
Windows daemon refuses a POSIX-absolute path and vice versa, and the host
filesystem never decides what a remote path means. `ReadError::reason()`
gives the refusal vocabulary one daemon-side home.
Read-only by construction: the toolbar carries Find alone, and save /
applyFresh / externalChange all return early for a diff record. Both models
are disposed before the editor on every path — a leaked one keeps its URI
registered and reopening the same path throws.
15/15 over a real daemon. "Side by side" is measured geometrically: a diff
editor has no getOption, so the probe's equal-width/adjacent-pane geometry is
the oracle. The collapse ruler renders only after Monaco's diff computation
settles, later than the first `.view-lines` paint — the wait gates on the
widget, not the paint.
The HIGH was a coverage defect, not a behavioural one: the "both models were
disposed" check was a literal `check(..., True)`, and its premise was false —
createDiff puts the per-open `uid` in each model URI, so a reopen can never
collide with a leaked model. Replaced with a count against Monaco's own model
registry; verified by mutation (removing the two disposes now fails with 6
leaked models, while the reopen check still passed).

Also: pin side-by-side against Monaco's `useInlineViewWhenSpaceIsLimited`
default, which silently swaps to the inline view under 900px and which
`renderSideBySide: true` does not defeat (now asserted at 700px too);
re-check tab liveness before mounting, so a tab closed during the two
round trips cannot leave an unreachable pane; refuse a tree object rather
than relay git's raw fatal with host paths; make the CLI's containment claim
true and table-drive its arms; separate the two branches of the binary sniff;
cover the format-less output branch; latch the refusal so one gesture flashes
once; scan attributes for icon-only mutators; give the row keyboard access.
Both phases accumulated their usage into a default `Usage` with
`add_tokens`, which drops `model` by design (it sums across records of
differing models). The phase line therefore recorded `model: unknown`,
the price table could not resolve it, and its cost fell out of the run
figure — the run footer showed a bare `+?` residue with real spend
behind it (~$2.25 on the 2026-07-25 run of this repo).

The execute phase already routes its resume loop through
`Usage::fold_usage` (the one place accumulated-usage model derivation
lives, ADR-0008 D8). Do the same here:

- the verify-gate repair loop collects one `Usage` per attempt and folds
  after the loop, no vendor fallback (ADR-0002 — that lives in the
  adapter);
- the protocol bounce runs at most once, so it takes the bounce's usage
  whole rather than summing it in.

The two existing runner tests already drive both paths; they now assert
the recorded model as well as the outcome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ejoins it (#313)

`ralphy init` creates a fixed roster of labels, but the runner applies
`needs-split` by literal name (phases.rs) and that name was never in the
roster — on a fresh repo the first bundle verdict fails with `'needs-split'
not found`, a non-transient error the runner only warns about. Adds the new
`needs-human-review` label (#313) and closes the same gap for `needs-split`,
so every label the runner can apply is one `init` creates.

Both consts move to `runner.rs` beside `STOP_BEFORE_LABEL`/`TRIAGE_AGENT_LABEL`
so the apply site and the roster read one definition. `needs-human-review` is
deliberately NOT in `HUMAN_GATE_LABELS`/`human_return_labels`: it lands on a
*closed* issue and must never re-enter the ADR-0014 blocker path. A new test
pins that, and another asserts every runner-applied label is in the roster —
the check whose absence let `needs-split` drift.

Also corrects `ready-for-human`'s spec description, which read "Agent finished
— waiting for human review and merge". That is the *opposite* of its role (a
pre-run human gate) and collides with what `needs-human-review` now means.

setup-pocock's `triage-labels.md` only documented the five renameable triage
roles, so the fixed operational labels appeared nowhere. Adds a second table
for them, flagged as not-renameable — remapping them there has no effect,
since the runner applies them by literal string.

The three tests hardcoding the roster size now derive it from the spec list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
paulocorcino and others added 28 commits July 27, 2026 22:27
ADR-0051 is amended for the fence detach of #344, ahead of any code.

Sec. 8 gains the decision: the plane answers "where is my work" but not
"I have a second monitor", and sec. 3 forbids the zoom that would let two
regions fit on one screen. So a fence detaches into a real `window.open`
popup, and the set of detached fences is per-client AND per-tab -- a third
lifetime beside the viewport offset, not a third copy. The popup is a
child of the tab, never writes the desk (an injected sink, not a flag
branched at each call site), and opens no consoles of its own.

Sec. 9 gains the paragraph that keeps this from reading as a
contradiction. What that section forbids is a client claiming on its own,
on a machine-driven path; a detach is one operator, one click, handing
over a slot the acting tab already held. It needs no new mechanism:
tearing the origin windows down closes their sockets, which releases the
slot, and the popup attaches by the ordinary path with no `takeover`
anywhere -- so the daemon's 409 still makes theft impossible.

Sec. 6 records that the popup holds a membership snapshot rather than
participating in centre-point derivation, sec. 7a puts `detach` beside
arrange and close and states that a detached fence is otherwise ordinary
on the plane, and sec. 10 records the load-bearing non-change: the daemon
learns nothing, which is what makes "another browser sees an ordinary
fence" true by construction.

CONTEXT.md gains the detached fence entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The console module decided WHERE to persist at each call site; a second
document rendering the same consoles could only be kept from authoring the
desk by a `detached` test every future edit must remember. The write moves
behind `WBDeskSink` (daemon / none) injected via `window.WBConsoleOpts`,
alongside the view store, the boot and the launcher — each defaulting to the
shell's current behaviour, so index.html is unchanged by construction.
detachFold(registry, event) -> { registry, effects } decides every detach
transition with no DOM and no storage: the four-popup cap, one popup per
fence (a second detach focuses the first), and a re-attach that is idempotent
because both the popup's beforeunload and the opener's closed-poll report it.
The registry is never mutated, so the caller commits a transition only once
its effects actually ran — a popup the browser blocked leaves it untouched.
The fence chrome gains a detach verb between arrange and close, and an emptied
fence keeps its name, rect and place in the list while carrying the glyph that
tells it from an empty one. detachFence opens detached-fence.html FIRST and
bails on a null handle, so the invariant holds on every exit path: either the
popup exists and the members are torn down, or neither.

A member leaves the plane without forgetting its desk record and without
closing its daemon session — the record is shared state a second client still
renders, and the socket close is the writer-slot release (ADR-0051 section 9).
The untranslated snapshot stays in the OPENER, so a drag inside the popup is
discarded on re-attach by never having been read.
…holes (#346)

Two HIGH findings, both tests that stayed green against gutted code:
scenario 5 asserted the popup offers no launcher using three selectors that
exist nowhere in the product and one that only a placeholder ever renders — and
the fixture had no placeholder, so deleting `canLaunch: false` changed nothing.
The fixture gains an `agent` member, and the shell is sampled before the detach
as the control. Re-attach was verified as geometry alone, which a placeholder or
a permanently parked window satisfies identically; it now asserts live terminals,
no parked strip, and a real keystroke reaching the child.

Product fixes alongside: removeFence refused on a detached fence (the × destroyed
the only way home), the handover reply made demo-aware, the re-attach message
trusting the proven owner rather than its payload, a rescue timer for a popup that
never completes the handshake, the poll armed before the teardown, detached members
pinned against desk eviction, afterLogin no longer respawning them, and pagehide
closing popups that would otherwise outlive their opener.
`wb-detach-link.js` owns the two browser facilities the lifecycle needs — the
tab-scoped registry and the same-origin broadcast channel — behind the same
inject-or-deny surface `wb-desk-sink.js` established, so the popup (which
inherits a COPY of its opener's sessionStorage) is incapable of acting on a
registry rather than merely careful about it.

`peerFold` takes its window as an argument, so the six-second expiry is driven
in microseconds by the node table instead of by a real clock.
The registry moves into the acting tab's session-scoped store behind the new
link seam, and the two documents re-find each other over a same-origin
broadcast channel after the opener's handle has died with its document. Boot
restores the registry SYNCHRONOUSLY, before the desk fetch is issued, so
`restoreDesk` already knows which members belong to a live popup and skips
every verdict for them — a `relaunch` there would have spawned a second PTY
against a console the popup drives.

The `pagehide` popup-close loop is deleted: it fires on a reload exactly as on
a close with no discriminator between them, which is the defect. Silence is the
single rule now, covering a clean close and a force-kill alike.

The initial handover is untouched — the members still ride #346's
concrete-targetOrigin postMessage; only lifecycle chatter uses the channel.
…death (#347)

Eight scenarios over a real daemon on port 7440. The no-flash oracle is a
MutationObserver installed by `add_init_script`, so it samples from document
start: "no console ever appeared on the plane" cannot be read after the page
settles. With every member inside the detached fence, its expected peak is
exactly 0.

`link()` no longer writes at load — the key's presence IS the fact "this tab
holds a detach", and a boot-time write would forge it for every tab, which is
what scenario 3 asserts against.
restoreDesk threw on the `adopt` verdict, whose record is null by construction
(`reconcileDesk`), and the swallowing catch turned that into a plane with no
fences and no glyphs at all — on any boot carrying a live session no record
claims, detached or not. Both guards are now null-safe and the null emission is
pinned, so the reason cannot rot.

`popup-here` replaced the map entry with a fresh object, which defeated the
glyph's ping comparison with the very reply it waits for: after a reload the
focus intent was unreachable. It mutates in place now.

The boot skip no longer derives membership from geometry. A detached fence is
still movable, and after a drag the fold compared its NEW rect with the members'
ORIGINAL records, answered "no members", and put every one of them back under
the live popup — a second PTY on a relaunch. The registry carries the member ids
instead; new scenario 5b drives the drag-then-reload path that found nothing
before.

Also: an `origin-close` verb, because a post-reload re-attach holds no handle and
had no way to evict its popup; a de-duplication against the plane for the
popup-gone-mid-boot race; the popup takes a channel-only link so it reaches no
store at all; and four oracles that could pass against broken code are now
discriminating.
`.gitignore` already carried `docs/screenshots/`, but a gitignore rule has no
jurisdiction over a path that is already in the index. The five PNGs on this
branch were force-added after that rule landed (b6726c1), so every acceptance
run that regenerated one showed it as a modification and offered it for upload.

Untrack them and narrow the rule to `docs/screenshots/*.png`. A screenshot is
evidence for one gate run: regenerated by the script that produced it, a new
blob per run, and nothing a reader can diff. The narrower pattern also stops the
folder from swallowing what is ordinary source — a README naming the
convention, an index — which a bare directory rule would have.

The files stay on disk; only the index changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Updated fence chrome to only display the name and console count, removing the repo list as fences are not bound to projects.
- Adjusted tests to reflect changes in fence chrome output, ensuring empty fences display "(0 consoles)".
- Enhanced toolbar list to show fence names and their respective Alt+Shift+F<n> accelerators, while ensuring console counts are read from the title bar.
- Modified glyph behavior to close the window holding consoles and return them to their original boxes, regardless of popup state.
- Improved test coverage for scenarios involving console management, including handling of closed consoles in popups and ensuring proper state management across multiple contexts.
- Updated documentation to clarify glyph functionality and its role in managing console visibility and state.
A saved AGENT console whose session is gone comes back as a placeholder
with a button, and that stays the default: loading a page must never
spawn a vendor CLI and spend quota nobody authorized. But the operator
may authorize it standingly — Settings -> Consoles -> "Relaunch agent
consoles on load". Plain shells are unaffected; they always came back by
themselves.

The preference lives in the per-client view store, not in the desk and
not in a repo's config. Daemon-wide, every tab pointed at the same desk
would relaunch the same consoles and spend the quota once per tab; in a
repo's settings.json, one operator's browser choice would bind every
other client. `reconcileDesk` takes it as a parameter and defaults it to
false, so the fold stays pure and a caller that forgets the option gets
the parked placeholder — never a launch.

Note what the placeholder's button was always doing: the old PTY is
gone, so it is a fresh LAUNCH, not a reconnect, with no scrollback to
come back to. The setting's help line says so.

The browser acceptance found one real defect on the way: the `relaunch`
verdict spawned `{ console: true }` unconditionally, which was correct
while only shells could reach it and opened a plain shell in the agent's
box once the opt-in let an agent record through. It now asks for the
record's own vendor, exactly as the placeholder's button does.

wb_desk_303.py grows scenario 10 (the toggle driven through the real
modal, both ways, against a real daemon) and three fold rows whose
control is the omitted option; wb_view_339.py's shape check admits the
new key, since the rule is "no desk in browser storage", not "no fourth
key". A lib.rs pin is the gate CI can see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the settings modal's foot note and right-align `Done` behind it, the
same way the About card's went. The head's subtitle claimed everything
below is saved per project, which the client-scoped section had just made
false — each section already carries a scope chip that says where it is
saved, so the subtitle now defers to it.

The relaunch-on-load help line said the same thing three ways. Two facts
change the decision: it is a launch rather than a reconnect, and it costs
one vendor CLI per console per load. That is all it says now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`/api/about` served `CARGO_PKG_DESCRIPTION`, so the card showed text
written for whoever opens the manifest: it cites an ADR path and the rule
confining tokio to this crate. An About card is not that reader.

The field goes from the route, the seed and the markup rather than being
reworded — a product line is a decision, and an empty space says less
wrongly than the crate's own note does. Cargo.toml keeps its description,
which still has a job where it lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The topbar brand read `identityName || 'ralphy'`, so a baptized daemon
replaced the product's name with its own — and the two were
indistinguishable here only because this host's daemon happens to be
called ralphy. The bar now says what you are looking at, fixed.

The daemon's identity keeps its place on the right, where it answers a
different question: the avatar on the account button, the chosen name
inside the menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three controls sit one pixel from something harmless on the same title
bar: tiling a fence rearranges every console in it, removing a fence
takes the region out from under them, and a console's × ends a live
session and its scrollback. All three now ask first.

The dialog is built in wb-console.js rather than borrowed from the
shell's Alpine one, because this module also runs in the detached-fence
popup, which carries neither Alpine nor the modal markup — and the same
click must ask the same question in both windows. It reuses the shell's
modal classes, so it is the same dialog to look at. `window.confirm` was
the other candidate and is worse than either: it blocks the thread, and
an automated browser dismisses it by default, which would quietly turn
every guarded click into a cancelled one.

The gate is on the BUTTONS, not on the verbs: `arrangeFence` and
`removeFence` stay exported and unguarded, because a caller that names
the verb has already decided. Two clicks skip the question outright — a
detached fence refuses both tile and remove and says why, and putting a
question before an answer that was never the operator's to give is
worse than not asking. Cancel takes the focus, so a stray Enter cannot
reproduce the very slip the dialog exists to catch.

The eight suites that drive these controls answer the dialog; #342 and
#303 gain the check that matters — cancelling moves nothing, closes
nothing, and leaves the daemon's session list alone. Answering `yes`
everywhere else would pass against an inert dialog too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three unrelated slips in the console chrome, reported together.

The account menu hangs from the far side of the top bar and enumerated
none of the toolbar's pickers, while they enumerated each other: opening
it over a live New-console menu left both on screen, overlapping. Every
toggler now goes through one `closeMenus`, so a fifth menu is one line
rather than four edits — and the acceptance drives it in BOTH directions,
because a fix that only holds the way it was reported is half a fix.

The Consoles tab wore a robot, which named the agents rather than the
plane they run on. It now wears the same terminal glyph as the
New-console button and the rows in its own menu. The button moves from
lucide's `square-terminal` to Bootstrap's `terminal` to get there: the
tab renders `t.icon` as a class, so that is the only glyph the two can
literally share, and they are the same picture to look at.

A fence's name was an always-live input sitting in the title bar the
operator also clicks to reach the fence — so any such slip was a rename,
silently committed on blur. It is read-only until a double click, and
shuts again on blur. #340 gains the half that matters: a single click and
a keystroke leave the name alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test job compiled the whole workspace under `[profile.release]`, whose
`lto = "thin"` and `codegen-units = 1` are a ship-time cost paid once for the
single binary `release.yml` builds. The job builds ~50 test binaries instead,
and `ralphy-cli`'s integration tests each link the entire CLI: that crate alone
spent over 20 minutes linking, and both runners hit `timeout-minutes` without
executing a single test. Measured cold on Windows, the whole gate now takes
3m11s to compile and 102s to run 2156 tests.

Optimization is what the tests need, not the linking — the daemon's PBKDF2
tests are the slowest in the suite — so `[profile.ci]` inherits `release` and
drops only `lto`, `codegen-units` and `strip`. The shipped artifact is
unchanged: `release.yml` still builds `--release`.

`Build` also gains `--all-targets`, so the test binaries are compiled in the
step named for building. A future compile blowup then fails there instead of
masquerading as a hang in the Test step, which is what the bound added in #295
exists to tell apart.
The Linux gate has been reporting "cancelled" with no log to read: a hanging
test sat until the hosted runner lost communication, and a dead runner uploads
nothing. Bounding the job cannot fix that — the bound needs a live runner to be
enforced against.

Give nextest the kill instead. It terminates the stuck test on its own, the run
ends normally, and the summary names it, so the next red gate says which test
rather than only that the runner died.

The slowest healthy tests are the daemon PBKDF2 hashes (~20s) and
`pins_skip_and_stop_vocabulary` (~49s), so nothing legitimate approaches the
360s kill.
…l(1)

The Unix tree-kill spent the pgid as text on `kill(1)`, which has to guess
whether a leading `-` opens an option or names a process group. procps-ng
guesses wrong: `kill -KILL -<pgid>` signals the *calling* process group. So
every `run_headless` teardown killed whoever asked for it. Locally that is the
test process, which is why all 78 `ralphy-adapter-support::headless` tests died
by SIGKILL on Linux; on CI it is the runner agent, which then uploaded no log
at all and left the gate red with nothing to read. Windows was never exposed —
that arm walks the process table by parent-PID.

This is not only a test fault. A shipped Linux build sent that same signal on
every agent teardown, so an unrelated process group on the operator's machine
could be killed mid-run.

`libc::kill` takes the negative pgid as a number, so there is nothing left to
misparse, and `libc` was already a Unix dependency of this crate. A pgid of 1
is refused rather than sent: `kill(-1, ...)` means every process the caller may
signal.

The regression test keeps a bystander in the caller's own group and kills a
group that no longer exists. It fails by SIGKILL against the old arm — the
blast radius is the assertion.
… rewrite

ADR-0053 plus the vocabulary it introduces (retry burn, unpriced volume, model
recovery). A ledger line whose `model` is `unknown` is not the price-table gap
ADR-0034 closes — it never recorded which engine spent the tokens, so there is
no key to look up. The repair is a read-time join to the session store the usage
scan already reads, kept in a persisted append-only map beside an append-only
ledger.

Committed as found in the working tree: this is design work that predates the
release, and it is carried here so it is not lost, not because rc16 depends on
it. Docs only — no code, and no effect on the shipped binary.
@paulocorcino
paulocorcino merged commit 7242e0a into main Jul 28, 2026
3 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