# Changelog OxygenHub runs at https://oxygenhub.io; each deploy's build is on the About page and at `/api/v1/version`. ## Unreleased - **Listing landed proposals is quick, and pages (#331).** `oh proposal list --state landed` on a repository with a few hundred proposals timed out: the API worked out every proposal's projected state again, landed and closed ones too. Those now come from what was recorded, and the list takes `limit` (`oh proposal list --limit N`, 30 by default), newest first. - **The API refuses a field an edit does not take (#330).** `PATCH` on an issue, proposal, project, milestone, release or drop answered 200 to a field it does not know and changed nothing (`{"ready": true}` on a draft proposal, say), which looked like success. It now answers 422 naming the field and the ones it takes. - **Mail reads `[OxygenHub]`, in a wider message (#334).** A notification's subject was tagged with the site's name (`[Oxygenhub on Earth]`); mail now goes by the From address's display name (`OxygenHub `) in the subject and the footer, falling back to the site's name, so the site keeps its own name (which its apt repository is labelled with). The message card is 720px wide rather than 560. - **An agent follows its owner's newest identity (oxygen#82).** A keyless certificate's answer now carries `author_chain`: the grant owner's newest linked identity and its predecessors back to the grant's key, so an agent home with `author.for` authors as the owner's current identity, not one several rotations old. - **The profile page's actions are one tidy group (#328).** Edit profile, Projects and Drops sat on two rows, touching each other and the panel below; they are now one evenly spaced block, Edit profile across the top and the two pages side by side. - **Copy buttons are a glyph joined to their field (#338).** Every copy button is now a clipboard glyph on the end of the box it copies, with the tooltip "Copy to clipboard"; pressed, it turns to a tick saying "Copied!" for two seconds. A drop's Embed and Clone boxes line up with each other. - **Changelog entries no longer block a landing (oxygen#145).** Two proposals that each add a line at the same place in a file marked `merge=union` in `.oxyattributes` (`CHANGELOG.md merge=union`) used to leave the second unlandable where conflicts are refused. When a proposal's only conflicts are such adjacent insertions, landing it now adds the resolution, both sides kept, the earlier change's first, authored by whoever lands it and signed by the hub as their delegate. Any other conflict blocks as before. ## 0.9.6 (2026-10-26) - **A highlighting test no longer fails a busy machine's gate (#327):** it timed a debug build against a release build's 500 ms; debug builds now have 3 s, still far below what highlighting the line would take. - **`oh runner-updates` and its API (#322):** site administrators read every level's update settings, the test groups, each runner's policy and offer, and the held builds (`GET /api/v1/admin/runner-updates`); `oh runner-updates set group:canary --updates on --window 02-05` changes one level, keeping what is not given (`PUT …/settings`, 422 saying what is wrong); `oh runner-updates lift 0.4.0` offers a held build again (`POST …/holds/{version}/lift`). - **A build that rolls back on one runner is held for the rest (#326):** when a runner reports it rolled back from flow X (it did not reach the hub after updating, or would not start), the hub offers X to no other runner, saying why, until an administrator lifts the hold on Admin › Runners › Updates, which lists each held build with the runner and the reason. One failed canary halts the rollout. Migration 0097. - **Finished work moves to Done (#325):** a card whose issue closes (on its page, through the API, or by a change's `closes #N`), or whose proposal lands or closes, moves to its board's column for finished work. A new board's Done is that column, and so is each existing board's column named Done; a column's menu turns it on or off (one column of a board). Archived cards stay archived. - **A WASM job's placement names no image (#323):** a job on the WASM tier was placed saying its runner "fetches the image from the hub"; it has none, so the line now starts with what the runner is doing (andymccall/windows-runner-demo run #1). - **Runner updates: policy, test groups and the offer (#322):** Admin › Runners › Updates sets where runner builds come from (a repository whose releases carry `flow-linux-ARCH` files, and `flow-windows-x86_64.exe` for Windows runners) and who takes them. Updates are off unless chosen; the site, an owner, a test group and a runner may each set updates on or off, a channel (`stable`, `canary` or a version), a window of hours (UTC) and dry run, and the nearest setting wins, each setting on its own. A test group is chosen runners or a percentage of the site's (or an owner's) runners, by a stable hash of each runner's id, so a share is the same machines every rollout and 10% sits inside 50%. `GET /api/v1/flow/runner/update` tells a runner its policy, where each setting came from, and the build to install (its BLAKE3, to check the download against); `POST` takes what the runner did, shown on the page and in the audit log. An organisation's owners set the same for their own runners in the organisation's Settings › Runner updates: the organisation's setting, test groups of its runners (chosen, or a percentage), and each runner's own; nothing of the site's or another owner's. Pins OxygenFlow's main. ## 0.9.5 (2026-10-26) - **No failed run every minute for a pipeline that does not parse (#321):** the scheduler asked each repository's pipeline every minute and, for one it could not read (a mistake, or no jobs yet), recorded a failed `schedule` run each time: oxygenhub-android had about 1,500. A scheduled tick now starts nothing for such a pipeline; its push's run still says what is wrong. - **A change's diff through the API (#320):** `GET /api/v1/repos/{owner}/{name}/changes/{hash}` gives each file's diff lines too (`lines`: `kind` context, add, del or gap, `old` and `new` line numbers, `text`), with three lines of context, up to 2000 lines a file (`truncated` says when it stops short), so the Android app can show a diff itself. - **JaCoCo coverage is read (#308):** Gradle's `jacocoTestReport` and Kover write JaCoCo XML, which a `coverage` artifact (or `POST /coverage`) may now be, beside LCOV and Cobertura: lines hit when an instruction ran, branches, methods. Its paths are relative to a source root (`io/oxygenhub/…/X.kt`), so a report's path not in the state is matched by its ending to the one file it can be. ## 0.9.4 (2026-10-25) - **Checkpoints, so a handed-back job resumes (oxygenflow#77):** `POST /api/v1/flow/jobs/{id}/checkpoint` keeps the directories a job declares with `checkpoint "/src/…"` as its runner hands it back; the next attempt's claim names the blob (`checkpoint`) and its runner fetches it (`GET`) and restores it before running. A checkpoint is kept beside the job's artifacts but is none of them (no receipt names it). Pins OxygenFlow's main. - **Boards keep the last fortnight of finished work (#293):** by default every column archives its finished cards (closed issues, closed or landed proposals) 14 days after they reached it, hourly, so a Done column stops growing however long a project runs; open work and notes stay. A column can instead archive every card after N days, or never ("Archive automatically" in its menu). Archived cards stay on the board's archive view, searchable and restorable. On the next hourly pass, boards with years of Done cards (Super Mario Land 2's 327) shrink to their last two weeks. - **An issue's references: no line through the chips, each change once (#318):** an opened "referenced by N changes" box drew the proposal timeline's vertical rule through its first column (the events shared its class); and a change was recorded again for every bookmark it was pushed on, and for every mention in its message, so one change filled several chips. Now one reference per change per issue or proposal, and the page shows each change once, including those recorded before. - **Claims say how long a job is expected to take (oxygenflow#76):** `expected_ms`, the median of the job's last five passing runs in the repository (reused passes left out), so a runner leaving with notice finishes a job that fits it and hands back the rest. Pins OxygenFlow's main. - **The hub's CI writes a test report and coverage (#307):** `test` runs under cargo-nextest and keeps a JUnit report (whether or not the tests pass); a new `coverage` job runs the tests instrumented beside it, with its own cached target, and keeps LCOV coverage. `coverage` is not a required check. - **The new proposal form lines up (#315):** as wide as the changes above it; Draft with Claude a secondary button under the editor (it fills the fields, it does not open the proposal); the draft checkbox and Open proposal on one row. - **Draft with Claude proposes a title too (#316):** for a proposal not yet opened, the assistant begins its draft with a title in the style of the changes' own titles; it fills the title field unless you wrote one (the page's own suggestion does not count), and stays out of the description. - **A waiting WASM job no longer says runners lack its image (#317):** a job with no image (the WASM tier) waited saying every runner "does not hold image" with an empty name; the explanation now skips the image check, as the claim always did. - **A draft proposal can be made ready from `oh` and the API (#313):** `oh proposal ready N` (and `--undo` to turn it back into a draft); `PATCH …/proposals/{n}` takes `state: "open"` for a draft and `state: "draft"` for an open one, as the page's buttons do, and `draft: false|true` in the words a proposal is created with. `{"draft": false}` used to answer 200 and change nothing. ## 0.9.3 (2026-10-25) - **Runners hand jobs back, and the queue takes them first (#312, oxygenflow#76):** `POST /api/v1/flow/jobs/{id}/release` {reason} lets a draining runner hand back a job it holds as it leaves (a phone off Wi-Fi, a restart, a spot reclaim). The job is queued again **ahead of new work**, whatever its priority; its log says who handed it back and why, and its next attempt's log follows on; it is not a decline. A job whose runner the hub has not heard from for 3 minutes (claims, 30-second heartbeats, logs) is handed back the same way, rather than running on until its time limit and ending as lost. A job handed back more than 5 times ends as an error saying so. Migration 0094 adds `flow_jobs.handed_back_ms` and `handbacks`. ## 0.9.2 (2026-10-25) - **`GET /api/v1/user/proposals?landable=true` (#311):** also lists the open proposals the caller could land now (in repositories they can write to, with no blockers, as each proposal's `landable` says), for the Android app to say when there is something to land. Only when asked for, since it works out each one's projected state. - **One logo, with its arms (#306):** the Oxygen mark's two arms were never drawn: their gradient was in bounding-box units, and a horizontal line's box has no height, so SVG paints nothing. The gradient is now in user space everywhere (header, footer, favicon, `mark.svg`; the VS Code extensions and the Android app too), and while a page loads the arms slide back and forth in turn beside the turning rings (still with reduced motion). - **Admin › Runners: the Serves column lines up (#309):** the scope sits above its input, so the inputs line up whatever the scope's length. - **A job's RE-RUN THIS JOB button no longer touches its log (#310).** ## 0.9.1 (2026-10-25) - **Pipelines may use the WASM tier:** OxygenFlow's main pinned (oxygenflow#75), so `runtime "wasm"` sandboxes are accepted and their jobs placed on runners that offer the tier (every runner from flow 0.3.1-3; an Android phone from the app). - **`POST /api/v1/user/runner-token` (repo:write):** a one-time registration token for a runner that serves the caller's own repositories and no one else's, as the Android app offers the phone as a runner (oxygenhub-android#8); it lasts a day, like the settings pages' tokens, and is audited. ## 0.9.0 (2026-10-25) - **`GET /api/v1/user/proposals` (repo:read):** the proposals the web dashboard lists, for apps: `review` (open ones by others in repositories the caller belongs to, not yet reviewed by them) and `authored` (their own open and drafts, in repositories they can still see), each with its repository, state, conflicts and web address. The Android app's dashboard shows them (oxygenhub-android#18). - **Board cards can be archived (#293, archive):** off the board but kept, on the board's archive view (`?archived=1`, with the same filter) and restored to the end of their column. A column archives its cards that reached it more than N days ago at once ("Archive cards here more than 14 days"), or on its own once set to (checked hourly, `projects.archiver`), so a Done column stops growing. An issue's or proposal's page says its card is archived. The API has `POST …/cards/archive` (#289's selection plus `older_days`, with `dry_run`), `GET …/cards?archived=true` and `POST …/cards/{id}/restore`; `oh project card archive N --from Done --older 14d`, `oh project card restore N CARD` and `oh project view N --archived`. Migration 0093 adds `project_cards.archived_ms` and `moved_ms` (when a card joined its column) and `project_columns.archive_after_days`. - **Board cards show their labels and assignees, and the board filters by them (#293, filters):** an issue's or proposal's card shows its labels (each a link to the board filtered by it) and an issue's assignees; the filter takes `label:NAME`, `assignee:NAME` and `state:STATE` with words, as the issue list does, every term holding. The board reads what its cards show once per repository rather than once per card (a board of 300 cards made 300 queries). The API's cards and `oh project view` carry the labels and assignees too. ## 0.8.0 (2026-10-25) - **Built on oxy 0.6.0 and OxygenFlow 0.3.1:** tag withdrawals, keyless grants that survive a key rotation, working depth-0 clones of big repositories (oxygen#134, #135, #137; oxygenhub#301). - **The API for apps (#302-#305), found building the Android app:** `GET /api/v1/user/repos` lists the caller's own repositories, their organisations' and those they collaborate on (`?affiliation=owner,organization,collaborator`, paged with `?after=owner/name&limit=`), and `GET /api/v1/user/orgs` their organisations and role (#302); a call with no token now answers 401 (sign in) rather than 403 (#303); `DELETE /api/v1/device/code` withdraws a device sign-in the app gave up on, at once (#304); each notification says its `repository`, `reason` (participating, watching), `thread` and the kind's `label` (#305). - **Boards that keep growing stay readable (#293, first part):** a column of more than 30 cards shows its newest 30 (cards join a column at its end), says how many older ones it leaves out and links to "show all"; a filter above the board finds cards by title or reference, and by state (open, closed, landed, draft), in every column, counting "3 of 312". - **An issue's references come in a box per day (#292):** an issue many changes refer to listed a line per change and ran on down the page. Two or more on one day are now one box, "referenced by 12 changes on 21 Oct 2026", their changes as wrapping chips, collapsible, the newest day's open. - **Move a board's column at once (#289):** `oh project card move N --from Doing -c Done` moves every card of a column, `--except 317,#320` leaves some, `--state closed,landed` takes only cards whose issue or proposal is done, and `--dry-run` lists what would move. One call (`POST …/projects/{owner}/{n}/cards/move`), all or none, one audit entry: tidying a board of 115 cards took 115 commands. - **Expediting a run moves the queue at once (#291):** a run's jobs kept the place they had ("9 of 9 jobs come before") until the next placement pass and some other change on the page. Expediting now places every queued job again straight away, every run whose jobs' notes changed updates live, and a job an expedited run moved down says so ("2 ready jobs come first (after expedited run andy/demo #12)"). - **Create and withdraw tags on the website (#295, #301):** a writer creates a tag on the Tags page (or `oh tag create`, `POST …/tags`), signed by the hub as their delegate, under the policy, and its tag jobs run; a maintainer withdraws one there (or `oh tag delete`, `DELETE …/tags/{tag}`, or `oxy push --delete tags/NAME`) with a signed withdrawal, never while a release uses it. The withdrawal is logged, audited and listed on the page with why, and clones drop the tag on their next fetch. Tags never move, and a withdrawn name is not used again. Pins oxygen with tag withdrawal and with keyless grants that survive their owner's key rotation (oxygen#135). - **An agent's changes verify in a new repository (oxygen#134):** a keyless agent's change in a repository its owner had never pushed to read INVALID ("grant is by an unknown author"). The hub now sends the grant's signer with each certificate, and with oxy's fix the agent keeps it and pushes it beside the certificate, so the repository can check the grant. - **A mistyped address shows the not-found page (#294):** an address the hub could not read, such as a project or issue number that is not a number, answered with a raw parse error ("Invalid URL: Cannot parse value …"); it now shows the site's not-found page, signed-in header and all. - **Embed a drop (#280):** a drop's page offers an `