> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentsfleet.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Stay up to date with the latest agentsfleet product updates, new features, platform improvements, and bug fixes.

<Tip>
  agentsfleet is in **stealth-mode testing** and pre-production. APIs and agent behavior may change between releases without long deprecation windows. Email [agentsfleet@agentmail.to](mailto:agentsfleet@agentmail.to) if you want a hand calibrating an agent or to join as a design partner.
</Tip>

<Update label="Jul 31, 2026" tags={["Breaking", "What's new", "API", "UI", "Security"]}>
  ## The isolation you assign is the isolation the runner applies

  The sandbox tier picked in **Add Runner** used to be a label: the host read a different value from its own environment file, neither side compared the two, and a host that could not deliver its claimed isolation kept accepting work. Policy now flows one way — the dashboard assigns it, the runner row stores it, and every heartbeat delivers it to the host — while the host probes what its kernel can actually enforce and reports that back. A runner whose assignment exceeds its capability is marked **degraded**, receives no work on either side, and its row names the exact missing mechanism (for example `cgroup controllers not delegated`) instead of showing a green badge over a host that refuses every job.

  ## Upgrading

  * **`POST /v1/runners` takes the assignment envelope** — the body is `{host_id, assigned_policy: {sandbox_tier, network_policy, registry_allowlist[], worker_count}, labels[]}`; the old top-level `sandbox_tier` answers `400` `UZ-REQ-001`. The response echoes the assignment as stored (`worker_count` clamped into `[1, 64]`).
  * **The runner environment collapses to the bootstrap pair** — `AGENTSFLEET_API_URL` and `AGENTSFLEET_RUNNER_TOKEN`, plus the optional host-local `RUNNER_STORAGE_HOME` (renamed from `RUNNER_WORKSPACE_BASE`). `RUNNER_HOST_ID`, `RUNNER_SANDBOX_TIER`, `RUNNER_NETWORK_POLICY`, `RUNNER_REGISTRY_ALLOWLIST`, `RUNNER_WORKER_COUNT`, and the `RUNNER_CP_*_MS` deadlines are removed, not deprecated — the daemon no longer reads them, so a stale env file cannot fork the truth again.
  * **`macos_seatbelt` leaves the tier vocabulary** — it never had enforcement code, and a tier that cannot be applied must not be assignable. A stray stored value parses fail-closed and the runner refuses to lease. The assignable tiers are `landlock_full`, `container_nested`, and `dev_none` (dev builds only).
  * **Existing runners read degraded once repaired** — pre-upgrade rows carry no assignment. A live, upgraded host marks its own row `degraded — no assigned policy` on its first heartbeat; a host that is down, or still running the pre-upgrade daemon, keeps a healthy-looking row until the manual statement below runs. Either way the fix is the same: open the runner and use **Edit policy**; the host applies it on its next heartbeat, no visit needed.
  * **One migration applies on startup, one statement runs by hand** — the migration adds the policy, capability, and verdict columns to `fleet.runners` and rewrites no rows. Right after deploying, mark the pre-upgrade rows degraded yourself (idempotent; it only matches rows without an assignment): `UPDATE fleet.runners SET degraded = TRUE, degraded_reason = 'no assigned policy' WHERE network_policy IS NULL;`

  ## What's new

  * **Add Runner assigns all four policy fields** — isolation, network policy (defaults `allow_all` until allowlist enforcement ships), registry allowlist, and worker count — with copy that says the host must satisfy the selection.
  * **Edit policy on the runner page** — `PATCH /v1/fleets/runners/{id}` with `{assigned_policy: {…}}` re-assigns a live runner; the change reaches the host within one heartbeat, and the verdict is re-checked in the same request. Growing the worker count past what the daemon started with takes effect after a runner restart.
  * **The runner row shows assigned against achievable** — the fleet list and detail reads carry `assigned_policy`, the host's reported `achievable` mechanisms, and `degraded` with its reason; a degraded row wears a badge and states what is missing.
  * **A runner that recovers, recovers by itself** — a later heartbeat whose report satisfies the assignment clears the verdict and leasing resumes; `UZ-EXEC-017` names the refusal while it stands.
</Update>

<Update label="Jul 31, 2026" tags={["Bug fixes", "CLI", "Security"]}>
  ## Steer never misses the reply's opening words

  `agentsfleet steer` now subscribes to the live event stream and confirms the subscription is established before it sends your message. A fast fleet could previously publish its first words before the terminal was listening — the command sat silent and recovered only the final text from history. The opening words now stream live on every run.

  ## Bug fixes

  * **Subscribe before send** — the steer turn opens its Server-Sent Events tail first and waits (up to 2 seconds) for the server to confirm the subscription; frames that arrive before the server names the event are buffered and replayed in order, and a tail that cannot open falls back to the previous post-then-poll behavior.
  * **Cancel before send** — Ctrl-C that lands before the message goes out now interrupts the turn without sending; the fleet no longer executes a message you cancelled.
  * **No stray connections** — every steer turn now provably closes its stream on success, failure, and interrupt alike.

  ## Security

  * **Dashboard toolchain refresh** — Next.js 16.2.12 clears three high-severity advisories (Server-Side Request Forgery in Server Actions, a middleware bypass, and a request-driven denial of service), and the dashboard now type-checks and builds on TypeScript 7's Go-native compiler.
</Update>

<Update label="Jul 30, 2026" tags={["What's new", "Performance"]}>
  ## Follow-up — chat replies start streaming immediately

  Fleet replies now appear in chat the moment the fleet starts answering. The runner used to hold the first activity frames of a run in a batch for up to a second before shipping them; it now ships the first frame of a run and the first response chunk the instant they arrive, so the chat surface shows the fleet is working — and its first words — at model speed. Long runs batch exactly as before, at a cost of at most two extra requests per run.
</Update>

<Update label="Jul 30, 2026" tags={["Breaking", "What's new", "API", "CLI", "UI", "Performance"]}>
  ## A runner has a page of its own, and every list pages by cursor

  A runner used to be a table row with a dialog of raw events behind an icon, and the only number on that dialog counted both halves of every execution — so a host that had run 4,000 events reported about 8,000 of something. Each runner is now an addressable page that opens on its leases: what it is working on right now, and for anything that failed, the reason in plain English with a link into the fleet whose work it was. Separately, no list in the platform pages by number any more. Page numbers silently repeat and skip rows whenever something is inserted mid-traversal, which on a host acquiring leases continuously is every few seconds.

  ## Upgrading

  * **`page` and `page_size` are removed from three reads** — `GET /v1/fleets/runners`, `GET /v1/fleets/runners/{id}/events`, and `GET /v1/api-keys`. All three take `?starting_after=&limit=` and answer `{"items": [...], "total": N, "next_cursor": "..."}`; follow `next_cursor` until it is `null`. A request still sending either parameter answers `400` `UZ-REQ-001` rather than being silently ignored, so client and server upgrade together.
  * **`sort` is removed from `GET /v1/fleets/runners`** — newest-first is the only order. Its non-default values existed to serve a sortable column on the table this release replaces, so the capability left with the control that used it. Sending `sort` answers `400` `UZ-REQ-001`.
  * **The fleets list renames its cursor on both sides** — the request parameter `cursor` becomes `starting_after` and the response field `cursor` becomes `next_cursor` on `GET /v1/workspaces/{workspace_id}/fleets`. Both old spellings are refused, not translated.
  * **The memory list envelope drops `request_id`** — it is now exactly `{"items": [...], "total": N, "next_cursor": "..."}`. Read the request identifier from the response header if you were consuming it from the body.
  * **`agentsfleet api-key list` no longer takes `--page` or `--page-size`, and `agentsfleet list` no longer takes `--cursor`.** The paging flags are gone rather than deprecated: an invocation carrying one fails as an unknown option and makes no request. Use `--starting-after` on `agentsfleet list`; `api-key list` needs no flag, since it now returns every key.
  * **Two index migrations apply on startup** — both add an index and touch no column, so no row is rewritten and no step is manual.

  ## What's new

  * **`/admin/runners` is a card wall, and each card is a link.** A card states what its host is working on in one line, or that it is idle, and shows administrative state before liveness so a cordoned host never reads as healthy.
  * **`/admin/runners/{runner_id}` opens on Leases** — a metrics strip over the standard table, live leases first, each row carrying its outcome. A failed row reads the same plain-English sentence the fleet console uses, never the machine tag, with the daemon's detail line beneath it. Activating a row opens Review lease: fencing token, kind, provider, model, posture, token meters, and expiry.
  * **Activity carries lifecycle records only.** Lease acquire and release are excluded because the lease table already states each of them once, with its outcome — which is what removes the doubled count.
  * **An expired lease is never credited with someone else's success.** Outcome is computed from the lease's own status first, so a lease this host stopped renewing reads expired even after another host finished the same work. A lease whose fleet event is missing reads as not recorded rather than as a success.
  * **`agentsfleet api-key list` returns every key you hold.** The list follows the cursor to the end instead of showing a first page, and the dashboard's key list drops its pagination footer while keeping column sorting.
  * **`agentsfleet memory list` takes `--starting-after <KEY>`**, and the dashboard's memory panel now shows every entry a fleet has learned rather than the first page of them.

  ## API reference

  * `GET /v1/fleets/runners/{id}` — requires `runner:read`. Returns the runner with derived liveness, `active_lease_count`, `active_fleet_count`, and lifetime `leases_acquired` / `leases_succeeded` / `leases_failed` / `leases_expired`, all computed from durable lease and event rows. Never returns `token_hash`. An unknown id answers `404` `UZ-RUN-014`.
  * `GET /v1/fleets/runners/{id}/leases?starting_after=&limit=` — requires `runner:read`. `limit` defaults to 50 and is refused above 100. Each item carries `outcome` (`running`, `succeeded`, `failed`, `expired`, or `unknown`), `failure_label` and `failure_detail` when it failed, `fencing_token`, `kind`, provider, model, posture, the three token meters, and the fleet and workspace identifiers plus the fleet name so a link needs no second read. Never returns `request_json`. An unparseable `starting_after`, or a limit outside the range, answers `400` `UZ-REQ-001`.
  * `GET /v1/fleets/runners/{id}/events?event_type=<tag>[,<tag>…]&starting_after=&limit=` — `event_type` now accepts a comma-separated set and returns the union. An unrecognised tag anywhere in the set answers `400` `UZ-REQ-001` and no partial result, so a typo can never read as "no such events". Follow `next_cursor` until it is `null` to read a runner's whole history.
  * `GET /v1/workspaces/{workspace_id}/fleets/{fleet_id}/memories?starting_after=&limit=` — all three query shapes (recent, category, and text search) page by cursor over creation order, so a filtered or searched list can no longer be silently truncated at the first page.

  ## Bug fixes

  * **The memory list's next-page hint is runnable.** It printed a command without `--fleet`, which `memory list` requires, so copying it produced a usage error instead of the next page. It now carries the fleet you asked about.
  * **Paging a runner's leases no longer costs its whole history.** The lease table accumulates a row per claim and is never pruned, so reading one page had been sorting everything that host had ever done. Both of the read's access paths are indexed now, and page cost stays flat as the history grows.

  ## CLI

  * `agentsfleet list [--starting-after <ID>] [--limit <N>]`
  * `agentsfleet memory list --fleet <ID> [--starting-after <KEY>] [--limit <N>] [--category <NAME>]`
  * `agentsfleet api-key list [--sort <FIELD>]` — no paging flags
</Update>

<Update label="Jul 29, 2026" tags={["Breaking", "What's new", "API", "CLI", "UI"]}>
  ## Replacing a secret means sending the secret you want stored

  A stored secret can never be read back, so changing one field by field could not be checked by the caller — and on most secret shapes the old field patch silently did nothing at all. Replacement is now total on every surface: the API takes the whole body, the client gets `agentsfleet secret update`, and the dashboard's Edit dialog is the same form as Add, prefilled.

  ## Upgrading

  * **`PATCH /v1/workspaces/{workspace_id}/secrets/{secret_name}` is removed** — replaced by `PUT` on the same route. `PUT` takes `{"data": {...}}`, the same shape `create` takes, and replaces the stored body whole; a field you omit is absent afterwards. There is no compatibility spelling, and a `PATCH` now answers method-not-allowed. Client and server upgrade together.

  ## What's new

  * **`agentsfleet secret update <name>`** replaces a stored secret in one call. The name stays claimed for the whole call, so fleets that require it keep resolving — no delete-and-recreate gap. It takes the same `--data` object (or `--data=@-` on stdin) and the same typed custom-endpoint flags as `create`.
  * **The dashboard's Edit dialog is the Create form, prefilled** — provider, base URL, and model come from the row you opened; you supply the key. A custom endpoint's base URL is editable after creation for the first time.
  * **Edit writes the model entry before the credential** — a credential is shared by every entry referencing it and can never be read back to restore, so it is written last. If the credential write is the one that fails, the model change that already committed is re-read into the table rather than left off screen.

  ## API reference

  * `PUT /v1/workspaces/{workspace_id}/secrets/{secret_name}` — request `{"data": {...}}` (non-empty object, at most 4 KiB stringified); `200` `{"name": "..."}`. A name the workspace does not hold answers `404` `UZ-VAULT-003` and creates nothing — a replace racing a delete cannot resurrect the deleted secret.
  * The write is one statement. Two concurrent replaces resolve to the later body whole; no merge of the two can be stored.

  ## Bug fixes

  * **Reconfiguring an existing custom endpoint from the Add dialog works again** — it still sent a create, which stopped overwriting on Jul 28, 2026, so reusing the name answered `UZ-VAULT-005` and saved nothing. It now replaces the endpoint's stored body.
  * **A failed save in Edit no longer strands the shared credential** — a save changing both the model and the key could commit the new key, then report failure, leaving every entry sharing that secret on a credential the dashboard said was not saved. The credential is now the last write, so a failure ahead of it touches nothing.

  ## CLI

  * `agentsfleet secret create` on a name that already exists still reports `skipped` and exits `0`; the message now points at `agentsfleet secret update` instead of delete-and-recreate.
</Update>

<Update label="Jul 28, 2026" tags={["Breaking", "What's new", "Bug fixes", "API", "UI", "CLI"]}>
  ## The dashboard loads the page you asked for, and a failed read says so

  The Fleet library gallery and the Models registry used to read everything they held before painting, then render a failed read as an empty one — a workspace whose library was merely unreachable was told it had none, with no retry and no way to tell the two apart. Both now load one page at a time, keep what is already on screen when a read fails, and state what they have not loaded. Creating a workspace secret has also stopped overwriting a secret that already holds the name.

  ## Upgrading

  * **Creating a secret no longer overwrites one that exists** — `POST /v1/workspaces/{workspace_id}/secrets` answers `409` `UZ-VAULT-005` when the name is already held, and writes nothing. Rotate with `PATCH /v1/workspaces/{workspace_id}/secrets/{secret_name}` instead. The database decides, so two concurrent creates on one name resolve to one `201` and one `409`. Reconnecting a provider and refreshing its token are unaffected — those are rotations, not creations.
  * **`agentsfleet secret create --force` is gone** — it relied on the endpoint upserting, which it no longer does, so the flag could only have failed. Passing it now exits non-zero before anything is sent. Creating a name that already exists reports `skipped` and exits `0`. Rotate in place with `PATCH /v1/workspaces/{workspace_id}/secrets/{secret_name}`, which holds the name for the whole call. The client has no rotate command yet, and deleting then creating through it leaves the name absent between the two calls — any fleet requiring it fails during that gap.
  * **`?q=` is gone from `GET /v1/models` and `GET /v1/workspaces/{workspace_id}/fleet-libraries`** — the substring filter published on Jul 27, 2026 had no caller on any plane. A request that still sends it is answered as though it were absent, so you get a page rather than an error. Page with `next_cursor` and filter client-side.
  * **`support_files` has left every API response** — the admin catalog was the last surface carrying it. The manifest is still written and stored on import, and the content-addressed bundle tar remains the authoritative file list, so no install behaviour changes.

  ## What's new

  * **The Fleet library gallery and Models registry load more instead of everything** — each states how many entries it is holding and that more remain, so a bounded page never reads as a complete list.
  * **Your place in the gallery survives a reload** — the cursor of the page you are on is written into the address bar. A reload, a shared link, or a Back out of a fleet lands where you were rather than at the first page.
  * **The model catalogue loads when you reach for it** — hovering, focusing, or opening the model picker fetches it, and an ordinary visit to Models does not. Hover is skipped on touch pointers and under `Save-Data`, where a hover is not a guess about intent.
  * **A deep link to a library entry that is not on the loaded page says so** — it neither errors the page nor quietly shows you something else.
  * **The Add-model dialog will not save against a secret list that never loaded** — it used to submit anyway, skipping the name-ownership check. Save now waits for the list, says what it is waiting for, and offers a retry when the read fails.

  ## API reference

  * New error code: `UZ-VAULT-005` (409) — a secret with this name already exists in the workspace.
  * `UZ-LIBRARY-003` narrows to a `limit` outside 1 to 100. Its search-length cause left with `?q=`; the code and its registry row are unchanged.
  * `GET /v1/workspaces/{workspace_id}/fleet-libraries` cursors are bound to the workspace and page size, no longer to a filter.

  ## Bug fixes

  * **Fleet chat replies render as they arrive** — a reply stayed invisible until you switched tabs or reloaded, while the typing indicator ran and the Live badge stayed green. Every streamed frame is named by its kind, and a named frame reaches only a listener registered for that name; the per-fleet stream listened for unnamed frames alone and dropped all of them.
  * **The Events tab scrolls again** — expanding a grouped run set left the page frozen in a non-maximized window. The table's scroll region contained the wheel on both axes while having nothing of its own to scroll vertically, so the page never received it.
  * **Sortable column headers read evenly** — the unsorted indicator was drawn heavier and taller than the sorted one, on every sortable column at once.
  * **Runner rows lost a copy button that copied nothing useful**, and now lead with administrative state before liveness.

  ## CLI

  * **`agentsfleet secret create` costs one request** — it no longer reads the secret list first to decide whether the name is free. The endpoint decides, which also closes the window where two creates of one name could both believe they were first.
</Update>

<Update label="Jul 27, 2026" tags={["Breaking", "What's new", "Bug fixes", "API", "CLI"]}>
  ## Model and Fleet library reads are bounded pages

  The model catalogue and the workspace Fleet library gallery used to return everything they held in one response. Both now return a page, ordered and resumable through a cursor, and the catalogue can be filtered and revalidated with an `ETag`.

  ## Upgrading

  * **`GET /v1/models` returns at most 50 models per request** — follow `next_cursor` until it is null. A client that reads only the first response now sees a truncated catalogue. `version` and `models` keep their names and shapes; `total` (always null) and `next_cursor` are added beside them.
  * **`GET /v1/workspaces/{workspace_id}/fleet-libraries` is a page too** — ordered `created_at DESC`, then platform entries before tenant entries, then id. Follow `next_cursor`.
  * **The gallery summary no longer carries `support_files`** — the manifest is not served on the workspace plane; the admin catalog (`GET /v1/admin/fleet-libraries/{id}`) remains its only reader. Nothing else moved off the card.
  * **Upgrade the `agentsfleet` CLI together with the server** — `agentsfleet install --library <id>` resolves an id against the gallery, and a pre-upgrade CLI reads only the first page, so it reports a valid entry past that page as missing.
  * **Bundle imports and catalog edits now enforce requirement ceilings** — at most 32 required credentials, 64 required tools, and 64 network hosts, with names up to 200 bytes and hosts up to 253. An import past a ceiling returns `413`; a catalog edit whose install-gate copy is over 32 entries, or longer than 500 bytes for one credential, returns `400`. Stored entries are not re-validated, so nothing already saved becomes unreadable.

  ## What's new

  * **Filter the catalogue** — `?q=` matches a case- and accent-insensitive substring across a model's id and provider, `?provider=` matches a provider exactly. `%` and `_` match literally.
  * **Conditional catalogue reads** — every answer carries an `ETag`, `Cache-Control: private, no-cache`, and `Vary: Authorization`. Send the tag back as `If-None-Match` and an unchanged catalogue answers `304` with no body.

  ## API reference

  * `GET /v1/models?limit=&starting_after=&q=&provider=` → `{version, models, total, next_cursor}`, plus `ETag` / `Cache-Control` / `Vary` on both `200` and `304`.
  * New error codes: `UZ-LIBRARY-001` (cursor this endpoint never issued), `UZ-LIBRARY-002` (cursor for different filters or page size), `UZ-LIBRARY-003` (`limit` outside 1..100, or `q` over 128 bytes), `UZ-LIBRARY-004` (catalogue revision unreadable), `UZ-LIBRARY-005` (a compliant response would exceed its body ceiling), `UZ-LIBRARY-006` (pool or query failure), `UZ-LIBRARY-008` (a secret-reference write lost a race to a concurrent delete).
  * A stale-but-valid cursor is not an error: it resumes from its boundary and may return an empty page with `next_cursor: null`.

  ## Bug fixes

  * **The dashboard Models page lists every model entry again** — the page sent no `limit` and ignored `next_cursor`, so a tenant with more entries than one page silently saw only the first.

  ## CLI

  * **`agentsfleet install --library <id>` reads the whole gallery** — it follows `next_cursor` until the id is found, so an entry past the first page installs instead of reporting itself absent.
</Update>

<Update label="Jul 26, 2026" tags={["What's new", "Bug fixes", "Performance"]}>
  ## Adding runners no longer slows the control plane

  An idle runner's poll now costs one bounded Redis read and no database round-trip. Idle cost tracks how many runners you run, never how many fleets exist, so adding execution capacity stops making the API slower for everyone.

  ## What's new

  * **Idle polls skip the database entirely** — a poll consults a readiness index that ingress writes when a fleet actually receives work, so a fleet holding nothing is never examined. Previously every poll walked every active fleet on the platform.
  * **Scheduled fires enter the fast path too** — the cron producer records readiness when it appends, so a scheduled run is picked up on the next poll instead of waiting for the background sweep to reach its fleet.
  * **Per-poll work is capped** — the candidate scan is bounded by `MAX_READY_CANDIDATES_PER_POLL`, alongside `NO_WORK_RETRY_AFTER_MS` so both knobs are visible together.
  * **Runner authentication is memoized for one heartbeat interval** — repeat calls from the same runner no longer re-read `fleet.runners`. A cordon, drain, revoke, or delete drops the entry on the machine that served it, and any other machine's entry expires within one heartbeat.
  * **New `/metrics` families for poll cost and readiness depth** — candidate-scan depth and per-poll database round-trips, unlabelled and rendered without touching a datastore.

  ## Bug fixes

  * **A message posted to a non-canonical fleet id is now rejected** — an uppercase, brace-wrapped, or dash-free `fleet_id` on `POST /v1/workspaces/{workspace_id}/fleets/{fleet_id}/messages` matched the fleet and returned `202`, then queued the event onto a stream nothing reads, so the message was never delivered.
  * **Stopping, deleting, or auto-pausing a fleet clears its readiness entry** — a fleet that left `active` kept an unreachable entry that held a slot of the bounded sample permanently; the approval gate's automatic pause now clears it the same way the stop and delete paths do. A stop keeps the fleet's stream, so a resume still finds its undelivered event.
  * **A stranded event no longer waits on unrelated traffic** — the background sweep re-derives readiness from the streams themselves, including entries appended but never delivered to any consumer, which the previous reclaim path could not see.
</Update>

<Update label="Jul 25, 2026" tags={["Breaking", "What's new", "Observability"]}>
  ## Metrics move to one namespace and standard names

  Exported metrics now use standard OpenTelemetry names and correct units on a single `agentsfleet_` prefix. Old names are not emitted alongside the new ones, so stored queries need repointing.

  ## Upgrading

  * **`/metrics` families that started `fleet_` now start `agentsfleet_`** — `fleet_triggered_total` becomes `agentsfleet_fleet_triggered_total`.
  * **Four metrics renamed** — `agentsfleet.credit.drained_nanos` → `agentsfleet.billing.credit.consumed` (`{nanocredit}`); `agentsfleet.run.duration_ms` → `gen_ai.invoke_agent.duration` (seconds); `agentsfleet.tokens.processed` → `agentsfleet.invoke_agent.token.usage` (`{token}`, split by `gen_ai.token.type`), with cached input separate as `agentsfleet.invoke_agent.cache_read.token.usage` — a subset of input, not a third total.
  * **Workspace and tenant identity no longer appear on metrics** — per-workspace cost stays a Postgres query against the execution-telemetry rows.

  ## What's new

  * **Bounded model attribution** — `gen_ai.provider.name` and `gen_ai.request.model` are attached while a series budget allows; past it the attribute is dropped, never truncated, and `agentsfleet_otel_attribute_omitted_total` counts the omission.
  * **Standard HTTP span keys** — `http.request.method`, `http.route`, and numeric `http.response.status_code`. The route is the matched template, so the concrete path, query string, and `Authorization` header stay out.

  ## Bug fixes

  * **Successful lease renewals now reach the credit metric** — renewals committed money but emitted no sample, understating consumption for any run that renewed.
</Update>

<Update label="Jul 23, 2026" tags={["What's new", "Bug fixes", "API"]}>
  ## Idle load tracks your work, not your history

  Database work now scales with current activity instead of total stored history.

  ## What's new

  * **Idle database load stays flat as accounts grow** — stored runners and events no longer make background work more expensive over time.
  * **Long lists stay responsive** — runner, credential, and API key lists limit their work to the requested page.

  ## Bug fixes

  * **A backslash in an actor filter no longer returns `500`** — an actor value containing `\` now matches the literal backslash and returns a normal page.
</Update>

<Update label="Jul 23, 2026" tags={["What's new", "Observability", "Performance"]}>
  ## A hung peer cannot hold a call open

  Outbound network calls now have enforced deadlines.

  ## What's new

  * **Telemetry export finishes predictably** — each request obeys a deadline, so a stalled endpoint cannot hold delivery open indefinitely.
  * **Exporter health is on `/metrics`** — queue depth and dropped-telemetry totals make an exporter outage visible.
  * **High-volume traces stay bounded** — routine success traffic is limited; runner rejections, server errors, and drops stay visible in metrics.
</Update>

<Update label="Jul 23, 2026" tags={["Breaking", "Bug fixes", "API", "CLI"]}>
  ## An identifier has exactly one spelling

  Every workspace, fleet, and event id is a lowercase Universally Unique Identifier version 7 (UUIDv7). The API and the `agentsfleet` CLI now reject a non-lowercase id, which used to be accepted and could then drift from its lowercase twin across caches and deduplication keys.

  ## Upgrading

  * **Lowercase any identifier your code stores or builds itself.** Ids returned by the API and CLI are already lowercase; act only where your code uppercases an id or hard-codes one in a fixture or webhook URL. Those requests fail with [`UZ-UUIDV7-009`](/api-reference/error-codes), or [`UZ-REQ-001`](/api-reference/error-codes) on inline-validating routes.

  ## API reference

  * **Uppercase and mixed-case ids return `400`** — [`UZ-UUIDV7-009`](/api-reference/error-codes), or [`UZ-REQ-001`](/api-reference/error-codes) on inline-validating routes. Handle both.
  * **The CLI rejects the same shape** — checked before the request leaves your machine.

  ## Bug fixes

  * **A clock outside the representable range fails loudly** — minting an id on a host set before 1970 returns an error instead of an id that sorts wrong.
</Update>

<Update label="Jul 23, 2026" tags={["What's new", "Bug fixes", "API", "UI"]}>
  ## A struggling fleet explains itself

  A failed run now names its cause instead of repeating "Failed a startup safety check", and identical failures collapse into one row.

  ## What's new

  * **Failures name their cause** — `Failed a startup safety check — no instructions configured` appears in Chat, the events table, and the live completion frame. A run from an older runner that records no cause still shows the plain-language sentence, never a guess.
  * **A repeating failure is one banner** — what failed, why, how many times, when it last happened, and what to do. It clears when the fleet recovers.
  * **Repeats collapse** — consecutive identical deliveries fold into one expandable row with a `×N` count and time range.
  * **Compact integration events** — one line with the full payload one click away; a recognized change proposal links to its source.
  * **Guidance where you can act on it** — a startup failure links to the fleet's Skill tab.
  * **One pagination control everywhere** — every console table pages the same way, and a page is shareable by link.

  ## API reference

  * **`core.fleet_events` gains `failure_detail`** — an additive nullable text column carrying the human-readable cause. The events endpoint returns it beside `failure_label`; both are `null` on a clean run.

  ## Bug fixes

  * **Zero metrics on a failed run dim** — `0` tokens, `$0.00` cost, and `0ms` no longer read as a real measurement.
  * **A long-open Chat tab stays bounded** — the live view keeps a recent window; full history stays in Events.
</Update>

<Update label="Jul 22, 2026" tags={["What's new", "UI"]}>
  ## Tables stay sortable and contained

  ## What's new

  * **Sortable columns** — click a supported heading to cycle ascending, descending, original order.
  * **Contained event history** — long lists scroll below a sticky heading.
  * **Consistent pagination** — client, cursor, and page-backed lists share one control and keep recovery navigation on an empty later page.
  * **Exact dashboard frame** — the header aligns edge to edge and page content owns viewport scrolling.
</Update>

<Update label="Jul 21, 2026" tags={["What's new", "Bug fixes", "API", "UI"]}>
  ## Fleet detail is now a focused workspace

  Fleet detail opens on Chat, and every other panel moves into fleet-local navigation. The summary keeps status, latest outcome, tokens, spend, duration, and waiting approvals visible.

  ## What's new

  * **The composer is always on screen** — only the conversation scrolls.
  * **Every message says who sent it** — a sender chip, a readable name, and the time. Yours read `Operator`; the fleet's read its own name.
  * **Events describe themselves** — an incoming event shows what arrived, such as `opened · owner/repo#539 — add retry with backoff`, with its payload one click away.
  * **Failures read as sentences** — in Chat, the summary, and the events table, instead of the runner's internal tag.
  * **Fleet-local views** — Events is fleet-scoped; Memory, Skill, Trigger, and Settings each get one destination.
  * **Approvals stay in context** — a waiting count opens workspace Approvals filtered to the current fleet.
  * **Trigger drops setup clutter** — source, filters, schedule, and latest delivery only.

  ## Bug fixes

  * **Messages send while the live feed is down** — a refused message shows as failed with a retry instead of sitting queued.
  * **Your own messages survive a reload** — they rendered as empty rows because Chat used the fleet's reply field for every entry.
  * **Incoming events no longer render as blank rows.**
  * **A lost connection recovers on its own** — Chat reconnects when the tab returns to the foreground or the network comes back.
  * **The API accepts a real request's headers** — the 4 KiB request-header limit answered `431` on any authenticated request crossing a proxy. The limit is now 16 KiB.
</Update>

<Update label="Jul 21, 2026" tags={["What's new", "Bug fixes", "UI"]}>
  ## The console puts steering first

  The fleet console opens on the steer thread instead of the source editor, and the wall drops its abbreviations.

  ## What's new

  * **Steer-first console** — the `SKILL.md`/`TRIGGER.md` source card is collapsed to its header until you open it; **Edit** keeps it open while a draft exists.
  * **A way back to the wall** — the console header gains a **← Fleets** link.
  * **Plain-words wall tiles** — footers read `$1.20 spent · 7 events`; a stale feed reads **not live**.
  * **Events in the standard table** — Time, Status, Fleet, Actor, Type, Summary, Tokens, and Duration, with cursor paging.
  * **The wall search box is gone** — it only filtered fleets already loaded in the page.

  ## Bug fixes

  * **Console columns no longer overlap** — long unbroken lines scroll inside their own block.
  * **The run-metrics strip says Time** — `Wall` collided with the Live Wall page name.
</Update>

<Update label="Jul 19, 2026" tags={["What's new", "API", "UI"]}>
  ## Delete revoked runners, and a seeded model catalogue

  ## What's new

  * **Delete a revoked runner** — revoke is the destructive step, delete retires the record. The trash action appears only on revoked runners.
  * **A pre-seeded model catalogue** — Anthropic, OpenAI, Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, xAI, Groq, and Mistral direct, plus the same models via Pioneer, Fireworks, Together, Novita, and OpenRouter. Each host carries its own pricing.
  * **Custom endpoints learn their context window** — an OpenAI-compatible endpoint naming a known model enforces that model's real context cap.
  * **Loading screens speak** — route loaders rotate wording; screen readers hear the plain wording.

  ## API reference

  New endpoint: `DELETE /v1/fleets/runners/{id}` (scope `runner:write`). Deletion cascades the runner's lease and event history.

  ```json theme={"theme":"vesper"}
  // 204 No Content on success
  // 404 UZ-RUN-014 — no runner with this id
  // 409 UZ-RUN-016 — active runner must be revoked before deletion
  ```

  ## Bug fixes

  * **No fake platform default** — a fresh tenant sees "No default is configured" instead of a green **Active** badge.
  * **Self-managed models say "Billed by provider"** instead of "Rates unavailable".
  * **The platform-default row is hidden** for tenants running their own model with no default configured.
</Update>

<Update label="Jul 17, 2026" tags={["Bug fixes", "UI"]}>
  ## No missing-page flash, and a Platform sidebar group

  ## Bug fixes

  * **No not-found flash on the workspace home** — the dashboard entry, workspace switcher, and workspace creation open the fleet wall directly.
  * **The refetch dialog reads as a refetch** — **Fetch update** opens its own dialog with the repository locked to the entry's source. Repointing a source stays under **Edit**.
  * **A finished onboarding step shows a check.**

  ## What's new

  * **A Platform sidebar group** — **Runners**, **Model library**, and **Fleet library** move out of Configuration. It shows only to an operator holding the matching scope.
</Update>

<Update label="Jul 16, 2026" tags={["What's new", "API", "UI", "Performance"]}>
  ## Fleet detail becomes the operating console

  Source editing, live steering, memory, run cost, and lifecycle controls land on one page.

  ## What's new

  * **Source editing** — read and edit `SKILL.md` or `TRIGGER.md` from the dashboard. A save takes effect on the next wake and keeps the fleet's memory; a concurrent save reloads the other change instead of overwriting it.
  * **Memory correction** — inspect learned entries and forget one. Deleting the fleet still deletes all its memory; editing its source does not.
  * **Run cost** — each event can show tokens, wall time, and settled cost. The recent rollup covers the latest 200 events within seven days and labels that bound; lifetime spend stays separate.
  * **Faster fleet lists** — lifetime event and spend counters are maintained on write.

  ## API reference

  * **Fleet detail** — `GET /v1/workspaces/{workspace_id}/fleets/{fleet_id}` returns source, triggers, lifecycle state, lifetime counters, and an `ETag` header.
  * **Event cost** — fleet event rows include nullable `cost_nanos`, sourced from settled billing telemetry.
  * **Memory forget** — `DELETE /v1/workspaces/{workspace_id}/fleets/{fleet_id}/memories/{key}` requires `fleet:write`, returns `204` when deleted and `UZ-MEM-004` when the key is absent.
  * **Stale-write protection** — fleet source and platform library `PATCH` may send `If-Match`. A stale fleet edit returns `UZ-AGT-014`, a stale catalog edit `UZ-CATALOG-005`; both are `412` and include the current `ETag`.
</Update>

<Update label="Jul 14, 2026" tags={["What's new", "API", "UI"]}>
  ## The fleet catalog stops overstating itself

  A catalog entry's status badge now matches what a workspace will actually get, and an operator can correct an entry without deleting it.

  ## What's new

  * **Draft-first onboarding** — creating or refetching a platform entry returns it to draft. Publish after review.
  * **Edit a catalog entry's identity** — name, repository, and ref are editable. Changing the repository or ref discards the stored bundle and returns the entry to draft; workspaces already running the fleet keep the bundle they installed. **Fetch update** fetches at the ref the entry names, not the default branch.
  * **An honest status column** — an entry published with no bundle shows as **Broken**, not Published.
  * **Consistent naming** — **Create fleet library** replaces "Add fleet"; the page reads **Fleet library**.
  * **Copy buttons where a value wants copying** — bundle hashes (the full hash), identifiers, secret names, cron schedules, webhook URLs, and one-time credentials. A failed write is reported on the button instead of showing "Copied" and costing you a one-time token.
  * **The dashboard counts every fleet** — installing and killed fleets appear in the summary tiles.
  * **The live thread shows tool calls and cost** — each response lists the tools it called with durations.

  ## API reference

  `PATCH /v1/admin/fleet-libraries/{id}` additionally accepts `name`, `source_repo`, and `source_ref`. A request whose `source_repo` or `source_ref` differs from the stored value sets `content_hash` to null and `visibility` to `draft` atomically; re-sending the stored value is a no-op. Changing the source and setting `published: true` in one call is refused with `UZ-CATALOG-002`. Malformed sources are refused with `UZ-BUNDLE-001`. The entry identifier is never patchable.
</Update>

<Update label="Jul 12, 2026" tags={["What's new", "API"]}>
  ## Platform operators onboard fleets from the dashboard

  Enter a GitHub repository as `owner/repo` under **Fleet libraries**, and `agentsfleet` fetches it, validates the bundle, and adds it to the catalog. Requires the `platform-library:write` scope.

  ## What's new

  * **Onboarding without a hand-built API request** — onboarding the same repository twice updates the existing entry rather than creating a second one.
  * **Four prebuilt fleets** — `github-pr-reviewer`, `platform-ops`, `zoho-sprint-daily-summarizer`, and `zoho-recruiter-daily-summarizer`. Each is a public repository under `agentsfleet`.
  * **A fleet's library identifier comes from its bundle** — read from the `name` in `SKILL.md`, not the repository path you type. Keep the repository name, the `SKILL.md` name, and the `TRIGGER.md` name identical.

  ## API reference

  * **`security-reviewer` removed from the prebuilt catalog** — it named a repository that was never published.
</Update>

<Update label="Jul 12, 2026" tags={["Breaking", "Bug fixes", "API", "CLI"]}>
  ## Removed CLI commands stop appearing in hints

  ## Upgrading

  * **`agentsfleet workspace delete` JSON output field renamed** — `{"deleted": "<id>"}` is now `{"removed_from_local_state": "<id>"}`. Update any script parsing it. The workspace and its remote data are unaffected by this command.

  ## Bug fixes

  * **Stale command hints removed** — error text no longer points at `agentsfleet install --from`, `agentsfleet secret add`, `agentsfleet workspace add`, or `agentsfleet tenant provider add`. `agentsfleet list` with no fleets points to `agentsfleet library` and `agentsfleet install --library <library_id>`; vault hints say `agentsfleet secret create <NAME>`.
  * **Clean `--help` output** — `agentsfleet --help` no longer prints a telemetry-shutdown timeout after valid help output.
  * **GitHub Pull Request events** — the signed per-fleet webhook accepts the full set of supported Pull Request actions.
</Update>

<Update label="Jul 11, 2026" tags={["What's new", "API", "CLI", "Integrations"]}>
  ## GitHub App events route to repository-bound fleets

  A connected GitHub App routes Pull Request and failed workflow-run events to active fleets that subscribe to the repository and event and hold an approved GitHub grant.

  ## What's new

  * **Repository-bound triggers** — add `repositories: [owner/repository]` beside `source: github` and `events` in `TRIGGER.md`; omission receives no managed App traffic.
  * **Scoped fan-out** — one delivery may wake several matching fleets in its workspace; wrong-repository, wrong-event, inactive, and unapproved fleets receive nothing.
  * **Verified installation ownership** — the callback verifies access to the claimed installation and refuses to move one already connected to another workspace.
  * **Replay recovery** — replay protection uses the signed payload body per fleet, so changing the unsigned delivery header cannot bypass it.

  ## API reference

  * `POST /v1/ingress/github` verifies the platform GitHub App signature, acknowledges signed `ping` checks, maps `installation.id` to a workspace, and normalizes supported events. Returns `202` after routing and `404` with `UZ-WH-022` when no fleet subscription matches.
  * `pull_request` events are accepted. `workflow_run` is accepted only for completed failed runs.

  ## CLI

  * **`agentsfleet connector list [--workspace <id>] [--json]`** — reports `connected`, `not_connected`, `reconnect_required`, or `unconfigured` per provider.
  * **`agentsfleet connector status <provider> [--workspace <id>] [--json]`** — one provider's state, without treating a valid disconnected state as an error.
</Update>

<Update label="Jul 11, 2026" tags={["Breaking", "API", "Security"]}>
  ## The model library is now an authenticated read

  Per-model pricing is no longer world-readable.

  ## Upgrading

  * **`GET /_um/<key>/cap.json` is retired — the path returns `404`, no alias.** Switch to `GET /v1/models` with a Bearer token. The `models[]` rows are unchanged: `id`, `provider`, `context_cap_tokens`, `input_nanos_per_mtok`, `cached_input_nanos_per_mtok`, `output_nanos_per_mtok`. The `?model=` filter is not ported — filter client-side. Dashboard and CLI users need do nothing.
  * **The global `rates`/`billing` block is gone with the document** — `GET /v1/models` carries the catalogue only. Current rates stay at [agentsfleet.net/#pricing](https://agentsfleet.net/#pricing).

  ## API reference

  * **`GET /v1/models`** — Bearer-authed, any tenant, no capability scope. Returns `{ "version": "YYYY-MM-DD", "models": [...] }`; an empty catalogue is `200` with `models: []`; a missing or invalid token is `401`.
  * **`UZ-PROVIDER-004`** — the "Model not in library" guidance points at `GET /v1/models`.
</Update>

<Update label="Jul 10, 2026" tags={["Bug fixes", "API", "Integrations", "Security"]}>
  ## Reconnecting an integration takes effect immediately, and Jira stays connected

  No API shapes, endpoints, or error codes changed.

  ## Bug fixes

  * **Stale token after reconnect** — the credential cache spots a changed identity, such as a different GitHub installation or a rotated personal access token, and mints a fresh token. It used to serve the cached one for up to an hour.
  * **Jira hourly reconnect loop** — the platform stores the rotated refresh token returned during renewal. Applies to any provider that rotates them (Atlassian's Jira OAuth does by default); Zoho Desk is unaffected. A failed store still lets the in-flight request succeed, at the cost of at most one reconnect prompt.
</Update>

<Update label="Jul 10, 2026" tags={["API", "Bug fixes"]}>
  ## Per-fleet spend ceilings are enforced

  `daily_dollars` was required in every `TRIGGER.md` and documented as a hard ceiling, but nothing read it. The ceiling now holds.

  ## What changed

  * **`daily_dollars` and `monthly_dollars` now stop a fleet** — a fleet that has spent its allowance is refused before its next run opens, recording `gate_blocked` with a `budget_breach` label. The refused event is **not** charged. A fleet crossing its ceiling mid-run stops at its next lease renewal (`UZ-RUN-015`) and records `fleet_error` with the same label.
  * **`budget_breach` is a distinct label** — previously every mid-run stop reported `renewal_terminate`.
  * **Spend windows** — `daily_dollars` is a rolling 24 hours, `monthly_dollars` the UTC calendar month; spend counts what you were charged. `monthly_dollars` stays optional.
  * **Lowering a ceiling takes effect immediately**, including on a run in flight.
  * **`/v1/admin/models` is documented** — the platform-admin catalogue routes were served without appearing in the API reference. A continuous-integration gate now fails whenever a served route is missing from the specification.

  ## Documentation corrections

  * **Continuation chains never existed.** The context-lifecycle page described a continuation chain capping at 10 and an 11th attempt labelled `chunk_chain_escalate_human`. None of it was built: a fleet that runs out of context wraps up, the run ends `processed`, and nothing re-queues it.
  * **`actor=continuation:<original_actor>` is not a value the runtime writes** — removed from the activity-stream and `--actor` filter documentation.
  * **`budget_breach` was documented before it existed.** It exists now.

  <Note>Every run is charged `$0` during the free-trial window, so no budget can be reached. The per-fleet ceiling and the tenant credit gate both begin to bite when the window closes.</Note>
</Update>

<Update label="Jul 10, 2026" tags={["Bug fixes", "UI"]}>
  ## Fleet timeline recovers frames lost to a connection blip

  ## Bug fixes

  * **Reconnect gap-recovery** — on reconnect the timeline re-fetches events published during the outage and merges them by event id, so a frame delivered twice renders once.
  * **Honest tail on failure** — a failed recovery fetch changes nothing on screen; the next reconnect retries. Nothing is fabricated to fill a gap.
  * **Conflicting style classes resolve last-wins** — components merge classes through one Tailwind-aware implementation.
</Update>

<Update label="Jul 09, 2026" tags={["UI", "API"]}>
  ## Admin model library: edit rates and set the default from a row

  ## What's new

  * **Edit rates in place** — the pencil on a catalogue row opens a dialog for the context cap and per-token rates; provider and model id stay fixed.
  * **Make any model the default from its row** — a star action sets the platform default (you enter the provider API key, stored in your vault).
  * **Consistent action icons** — every Create button shows a plus icon, and the fleet Install button a download icon.

  ## API reference

  * `GET /v1/admin/platform-keys` — each row now includes `model`, the priced `(provider, model_id)` the default resolves to (`null` for a provider that has been stood down).
</Update>

<Update label="Jul 08, 2026" tags={["UI", "API"]}>
  ## Models page becomes a registry

  Four fixed slots become a sortable table: register three Anthropic models on one key, or the same model on two hosts.

  ## What's new

  * **One row per configured model** — sorted by model or provider, with the platform default pinned first.
  * **Key names are identities** — a key name you already stored (same provider) updates that key in place and points the new model at it, so models share one credential. A name belonging to a different provider's key is rejected, not overwritten.
  * **Custom endpoints can go keyless** — the API key field is optional.
  * **Per-row actions** — View details (provider, endpoint, key name, created date — never the key), Edit, and Remove (blocked with a reason on the row powering inference).
  * **Switch with one click** — an inactive row's Switch button activates it immediately.

  ## API reference

  * `GET /v1/tenants/me/models` — lists registered entries, each joined to its key's provider/kind/endpoint and an `active` flag; also returns `platform_default_available`.
  * `POST /v1/tenants/me/models` — registers `{model_id, secret_ref}`. `secret_ref` is required and names the stored provider key or keyless custom-endpoint record. `404 UZ-MODELS-002` when it names no stored key; `409 UZ-MODELS-003` on an exact duplicate.
  * `PATCH /v1/tenants/me/models/{id}` — changes an entry's `model_id`. `404 UZ-MODELS-004` when the id isn't yours.
  * `DELETE /v1/tenants/me/models/{id}` — removes an entry; idempotent. `409 UZ-MODELS-001` when it's the active entry.
  * `PUT /v1/tenants/me/provider` also registers the matching model entry when none exists.
  * Activating a self-managed model takes the model from the `PUT` body or the registry entry, never the stored key. An activation that resolves no model returns `400 UZ-PROVIDER-004`.
  * Deleting a stored key still referenced by a model entry returns `409` naming how many entries reference it.
</Update>

<Update label="Jul 07, 2026" tags={["UI"]}>
  ## Crisper cards, clearer isolation modes, per-account avatars

  ## What's new

  * **Sharper cards and tables** — resting-state borders and card backgrounds are brighter.
  * **Pinned table header on Billing usage** — the "Date / Amount / Type / Description" header stays visible while scrolling.
  * **Isolation mode as option cards** — Settings → Runners → Create runner picks its mode from four described cards instead of a dropdown.
  * **A distinct account avatar** — with no uploaded photo, your avatar is a pattern generated from your account.
</Update>

<Update label="Jul 06, 2026" tags={["Bug fixes", "API"]}>
  ## Error responses stop leaking internal names

  ## Bug fixes

  * **No more raw internal error names** — an authentication-middleware failure returned a raw identifier (e.g. `error.TokenExpired`) in `detail`; it now returns a curated message, with the raw identifier logged server-side only.
  * **Plainer failure messages** — around three dozen internal-failure responses now return plain English; several previously-generic connector and provider failures return distinct, diagnosable codes.
  * **Model-provider errors say "library"** — they previously said "catalogue".
  * **`UZ-AUTH-014` is now 409 Conflict** — submitting a login verification code before approving the session returned 410 even though the session was still approvable.

  ## API reference

  * The [error codes reference](/api-reference/error-codes) is generated from the backend error registry on every release, so it cannot drift.
</Update>

<Update label="Jul 06, 2026" tags={["Breaking", "Security", "API"]}>
  ## Fleets need an approved integration grant before minting provider tokens

  Connecting GitHub, Zoho Desk, Jira, or Linear authorizes your workspace, not every fleet in it. Such a fleet must hold a human-approved grant, checked at lease and again on every token request. **Slack is not affected** — its bot token is long-lived and delivered directly.

  ## Upgrading

  * **Existing GitHub/Zoho/Jira/Linear fleets without a grant stop minting** after this release. One-time fix per fleet: `POST /v1/workspaces/{workspace_id}/fleets/{fleet_id}/integration-requests` (body `{"service": "github", "reason": "..."}`), then approve it from the dashboard. Already-approved fleets are unaffected.
  * **Slack fleets and static secrets** you pasted yourself (`${secrets.<name>.<field>}`) are not gated.

  ## What's new

  * **Grants are requestable for `github`, `zoho`, `jira`, and `linear`** — the endpoint previously rejected these services.
  * **A refused mint returns [`UZ-GRANT-001`](/api-reference/error-codes)** (403, "No integration grant for service") with the request-grant endpoint in the hint.
  * **Revoke works mid-run** — revoking refuses the fleet's next token request even while its run is leased; re-approving restores minting with no reconnect.
</Update>

<Update label="Jul 05, 2026" tags={["What's new", "Integrations"]}>
  ## A guide to connecting GitHub, Slack, Zoho Desk, Jira, and Linear

  ## What's new

  * **[Connectors guide](/fleets/connectors)** — the connect round-trip, the GitHub App-install exception, and which providers issue a refresh token.
  * **Secrets page** links to the guide and calls out the difference between a pasted vendor key (`${secrets.<name>.<field>}`) and a connector.
</Update>

<Update label="Jul 05, 2026" tags={["UI", "Bug fixes"]}>
  ## Runners, Model library, and Secrets adopt the standard table

  ## What's new

  * **Runners page** — renders in the same table as API keys and secrets. Enrolling is the "Create runner" dialog, which shows the install token once with a notice that it won't be shown again.
  * **Runner labels** — "Host id" is now "Host name"; "Sandbox tier" is now "Isolation mode" with plain options: Linux · Landlock, Nested container, macOS · Seatbelt, and None.
  * **Model library** — "Model rates" is renamed "Model library" across the nav, page, and dialog.
  * **Secrets** — "Secrets & ENVs" is now just "Secrets". Renaming a secret is its own dialog, opened from the Name column.

  ## Bug fixes

  * **Model library token counts** — large counts rendered inconsistently between server and client (a locale mismatch).
</Update>

<Update label="Jul 05, 2026" tags={["Bug fixes", "UI", "API"]}>
  ## API Keys stands alone, Buy credits works, library copy drops "template"

  ## What's new

  * **API Keys page** — the sidebar's "Workspace" entry is now "API Keys", a standalone page; `/settings` redirects here. Switching and creating a workspace stay in the top-right switcher.
  * **Buy credits** — renamed from "Purchase credits" and now a live `mailto:` link instead of a disabled button.
  * **"Create fleet library"** replaces "Add library entry" across the dashboard and CLI empty states.
  * **Left-nav active indicator** — the selected item shows a left accent bar.
  * **[Scopes reference](/api-reference/scopes)** — a new page listing every scope a tenant token can carry, linked from `UZ-AUTH-022`.

  ## Bug fixes

  * **Models page** — the active-model row no longer glows permanently (a `[data-live]` selector matched the "not live" state too); its redundant "Switch to platform defaults" button is removed.
  * **`UZ-PROVIDER-009`** — switching back to platform defaults when none is configured returns plain English instead of leaking "operator action required".
</Update>

<Update label="Jul 05, 2026" tags={["What's new", "Breaking", "UI", "CLI", "API"]}>
  ## Fleet library replaces Templates, Secrets gets its own page

  ## Upgrading

  * **`agentsfleet templates` → `agentsfleet library`; `--template` → `--library`.** No alias — update any script or continuous-integration job calling the old command or flag.
  * **`agentsfleet credential` → `agentsfleet secret`.** Same for `credential add`/`list`/`show`/`delete`.
  * **Routes renamed, no deprecation window:** `/v1/admin/fleet-templates` → `/v1/admin/fleet-libraries`; `/v1/workspaces/{workspace_id}/fleet-templates` → `.../fleet-libraries`; `/v1/workspaces/{workspace_id}/credentials` → `.../secrets`. Wire fields `platform_template_id`/`tenant_template_id` → `platform_library_id`/`tenant_library_id`.

  ## What's new

  * **Fleet library** — "Add library entry" replaces "Create a template"; the `?library=` deep-link query param replaces `?template=`.
  * **Secrets & ENVs** — vault secrets get their own sidebar entry and page.
  * **Models page collapses to one list** — the platform-default model renders as a normal row. The "Other provider" form's Provider field is a dropdown of real catalogue models instead of free text.

  ## Bug fixes

  * **Friendlier errors** — every error response can carry an optional plain-English `user_message` field (RFC 7807 `problem+json` body). It is written for \~15 codes: `UZ-PROVIDER-001..004`, `UZ-VAULT-001..003`, `UZ-BUNDLE-001..002`, and `UZ-APPROVAL-001..006`. A code without one omits the field rather than nulling it, so untouched responses are byte-identical.
</Update>

<Update label="Jul 04, 2026" tags={["Bug fixes", "UI"]}>
  ## Readable account settings, a copyable workspace ID, consistent labels

  ## What's new

  * **Copy the workspace ID** — the **Workspace** page lists the workspace name and ID as labelled rows with copy buttons. The ID is the value the `agentsfleet` CLI and the API expect.
  * **One label size** — section headers, table columns, and sidebar groups share a single type size.
  * **Clearer first-run screens** — Dashboard, Fleets, and Models empty states lead with **Learn more** plus one primary action.

  ## Bug fixes

  * **Account settings dialog** — headings, your email address, and the close button rendered dark-on-dark on the dark theme.
</Update>

<Update label="Jul 03, 2026" tags={["What's new", "API", "UI"]}>
  ## Connect Grafana, Zoho Desk, Jira, Linear, Fly, and Datadog

  Six connectors join GitHub and Slack. Each connect lands a vaulted credential the platform mints short-lived tokens from, and every vendor call runs under an enforced deadline.

  ## What's new

  * **OAuth connectors** — Zoho Desk, Jira, and Linear connect with a browser OAuth round-trip and store a refresh token. Jira resolves its Atlassian cloud instance during connect.
  * **API-key connectors** — Datadog, Grafana, and Fly take an operator-supplied key, checked by a live validation probe before it is vaulted; a rejected key returns `UZ-CONN-005` and nothing is stored.
  * **Connector catalog** — `GET /v1/connectors` lists every provider with its archetype and whether it is configured and connected.

  ## Bug fixes

  * **Bounded broker calls** — token exchanges are deadline-armed and fail closed.
</Update>

<Update label="Jul 03, 2026" tags={["Bug fixes", "CLI"]}>
  ## Reliability fixes for logs, credential help, and approval enforcement

  * **`agentsfleet fleet logs`** — a malformed event timestamp no longer throws `RangeError` and aborts the command; that row renders `—` and the stream continues.
  * **`agentsfleet workspace credentials`** — the redirect points at the real `agentsfleet credential` command group instead of a non-existent `agentsfleet agent credential`.
  * **Approval enforcement** — an approved or denied grant is honored by the fleet's lease path even when the Redis mirror of that decision fails to write; the durable database row is the fallback.
</Update>

<Update label="Jul 03, 2026" tags={["Bug fixes", "API"]}>
  ## Error reference matches the server

  * **`UZ-PROVIDER-003` hint** — now states that `provider` and `model` are required and `api_key` is required only for a named provider (optional for an `openai-compatible` endpoint). The old wording had clients sending an unnecessary `api_key`.
  * **Error-code reference** — removed the `UZ-AUTH-009` and `UZ-AUTH-010` rows, which have no producer in the server registry (both superseded by `UZ-AUTH-022`). The retired `UZ-AUTH-021` stays as a struck-through historical entry.
</Update>

<Update label="Jul 03, 2026" tags={["What's new", "UI"]}>
  ## Add a workspace template from the dashboard

  The Fleets install gallery gains **Add template**: enter `owner/repo`, the dashboard validates the required `SKILL.md`, adds the workspace template, and returns you to the gallery.

  ## What's new

  * **Dashboard onboarding** — **Add template** calls the workspace template onboarding API and refreshes the gallery.
  * **Empty states** — Fleets and Events use simple "No … found" copy with direct links.
  * **Models navigation** — the nav labels the page **Models**, and **Bring your own key** uses the same primary button style as **Install fleet**.
  * **Route motion** — route changes no longer wobble.
</Update>

<Update label="Jul 03, 2026" tags={["What's new", "API"]}>
  ## Operator dashboard access follows the same scopes as the API

  The runner fleet and model catalogue are gated on explicit `resource:action` scopes, the same ones the API enforces. The separate platform-admin flag is gone.

  * **Error code** — a request missing an operator capability returns `UZ-AUTH-022` (Insufficient scope), whose `detail` names the required scope; the former `UZ-AUTH-021` is retired.
  * **Scope hierarchy** — a held higher scope satisfies a lower one on the dashboard exactly as at the API (`model:admin` covers `model:read`, `runner:write` covers `runner:read`).
  * **Marketing-site analytics** recovers from a transient load failure instead of staying dark for the rest of the visit.
</Update>

<Update label="Jul 02, 2026" tags={["What's new", "API", "UI", "Integrations"]}>
  ## Slack: mention @agentsfleet in a channel, get an answer in the thread

  Connect Slack from the dashboard Integrations page, invite `@agentsfleet` to a channel, and mention it. The first mention creates a fleet resident in that channel. It is reactive and read-only: it speaks only when mentioned, and only in the thread that asked.

  ## What's new

  * **Connect Slack from Integrations** — the callback stores the bot token in the workspace vault. The row shows "Slack connected: \{team}" and offers **Reconnect** if the install is revoked.
  * **One fleet per channel** — later mentions in any thread reuse it. Memory is channel-scoped.
  * **Answers land in-thread** — never at channel top level, never in another channel.
  * **Recent thread context** — each mention re-reads up to 20 recent messages in its thread. Best-effort: if Slack throttles the read, the mention still processes.

  ## API reference

  * **`POST /v1/workspaces/{workspace_id}/connectors/slack/connect`** (scope `connector:write`) — returns the Slack authorize URL carrying a signed single-use `state`; **`GET /v1/connectors/slack/callback`** finishes the round-trip and vaults the bot token.
  * **`GET /v1/workspaces/{workspace_id}/connectors/slack`** (scope `connector:read`) — status as `{status, team}`, where `status` is `connected`, `reconnect_required`, or `not_connected`; never a secret.
  * **`POST /v1/connectors/slack/events`** — the signed events ingress, authenticated by Slack's request signature (a hash-based message authentication code over the raw body, 5-minute replay window), not a Bearer token.
  * **New error codes:** `UZ-SLK-010` (401, invalid signature), `UZ-SLK-011` (401, stale timestamp), `UZ-SLK-020` (200, event from a team with no install is acknowledged and ignored), `UZ-SLK-022` (502, OAuth token exchange failed), `UZ-SLK-030` (502, answer delivery failed — logged and retried; the run never fails). `UZ-CONN-001`/`UZ-CONN-002` now cover Slack as well as GitHub.
</Update>

<Update label="Jul 02, 2026" tags={["What's new", "API", "Integrations"]}>
  ## Connector platform: one uniform API, bounded vendor calls

  Connecting a third-party service runs through one documented route set instead of a per-provider one. The URLs you already use are unchanged.

  ## API reference

  * **`POST /v1/workspaces/{workspace_id}/connectors/{provider}/connect`** and **`GET /v1/workspaces/{workspace_id}/connectors/{provider}`** — start a connect and read status for any registered provider (`slack`, `github`), Bearer-authed with `connector:write` / `connector:read`.
  * **`GET /v1/connectors/{provider}/callback`** — the vendor redirect target; Bearer-less, authenticated by the signed single-use state minted at connect.
  * **`UZ-CONN-003`** (502) — an outbound vendor call hit its enforced deadline, could not be time-bounded, or the vendor was unreachable. Transient — retry.
  * **`UZ-CONN-004`** (404) — the `{provider}` matches no provider on this deployment; the body names it.
</Update>

<Update label="Jun 30, 2026" tags={["Breaking", "What's new", "API", "UI", "CLI"]}>
  ## Bring your own Fleet templates

  Onboard your own Fleet templates alongside the curated platform catalogue. Templates are stored once, content-addressed, and shared across the fleets you spin up from them.

  ## Upgrading

  * **Install is template-only.** `agentsfleet install --from <path>` has been removed. Install by id instead: `agentsfleet install --template <id>`. To run a fleet you authored, onboard its `SKILL.md`/`TRIGGER.md` as a template first. To push local edits onto an already-installed fleet, use `agentsfleet fleet update <fleet_id> --from <path>` (unchanged). Upgrade the CLI and server together.
  * **Fleet-create takes a template id, not a bundle.** `POST /v1/workspaces/{workspace_id}/fleets` accepts exactly one of `{platform_template_id}` or `{tenant_template_id}`, plus an optional `name`. Raw `source_markdown`, the per-workspace `bundle_id`, and the github-import-at-create body are rejected.
  * **Bundle import/snapshot endpoints removed.** `POST /v1/workspaces/{workspace_id}/fleets/bundles/snapshots` and the snapshot-detail read no longer exist.

  ## What's new

  * **Two tiers** — onboard a template into the global platform catalogue or into your own workspace; workspace templates are visible only to your workspace.
  * **One gallery, both tiers** — the install gallery unions the platform catalogue with your workspace's own templates.
  * **Templates explain their credentials** — entries carry a description and per-credential "why this is needed" copy.

  ## API reference

  * **`GET /v1/workspaces/{workspace_id}/fleet-templates`** — the workspace gallery. Each entry carries `id`, `name`, `description`, `visibility` (`"platform"` or `"tenant"`), `source_ref`, `requirements`, `required_credentials_reasons`, and support-file summaries — never an object-store key.
  * **`POST /v1/admin/fleet-templates`** (scope `platform-template:write`) and **`POST /v1/workspaces/{workspace_id}/fleet-templates`** (scope `template:write` + workspace ownership) — onboard from a GitHub source. The canonical bundle is written to object storage keyed by content hash; the response is metadata only.
  * **`UZ-BUNDLE-002`** now reads "No installable template or stored snapshot matches the request in this workspace."

  ## CLI

  * **`agentsfleet install --template <id>`** resolves the template in your gallery and installs it by tier. `--from <path>` has been removed from `install`.
  * **`agentsfleet fleet update <fleet_id> --from <path>`** is unchanged.
</Update>

<Update label="Jun 30, 2026" tags={["What's new", "API"]}>
  ## Authorization is now scope-based

  Every capability a token holds is an explicit `resource:action` scope read off the token, replacing the old `user`/`operator`/`admin` roles and the `platform_admin` flag. A correctly-configured principal sees no change.

  ## API reference

  * **`scopes` claim** — the session token and tenant API key carry a space-delimited `scopes` list (e.g. `fleet:admin credential:write workspace:admin`), set automatically for a workspace owner at signup.
  * **`403 UZ-AUTH-022` "Insufficient scope"** — a denial names the required scope in the detail (`Requires scope fleet:admin`).
  * **`read < write < admin` hierarchy** — per resource, `admin` satisfies `write` satisfies `read`.

  See the [error-codes reference](/api-reference/error-codes) for `UZ-AUTH-022`.
</Update>

<Update label="Jun 28, 2026" tags={["What's new", "UI", "API", "Bug fixes"]}>
  ## Template installs explain why a credential is needed

  A missing required credential now tells you *why* — "this fleet needs github to review your pull requests" — instead of a bare "connect github".

  ## What's new

  * **Purpose-driven connect step** — the install gate reads a per-credential reason from the template; templates without reason copy fall back to the generic prompt.
  * **No skippable installs** — the dashboard no longer offers a way past a missing credential.
  * **Fleets empty state is the gallery** — the first-run page shows the full-width template gallery.
  * **Custom secrets copy** — values are encrypted and write-only once saved: replaceable, never viewable.

  ## API reference

  ```
  GET /v1/fleets/bundles   each template row now carries required_credentials_reasons
  ```

  * **`required_credentials_reasons`** is an optional object keyed by credential name — `{ "github": "review your pull requests" }` — carrying display-only copy. It does **not** replace `required_credentials`: install validation still reads that array.
  * **Installing with a missing credential** still returns `UZ-BUNDLE-003` (424) with a `missing_credentials` list.

  ## Bug fixes

  * **Billing empty state** reads "No charges yet" instead of "No billable events yet".
  * **Dashboard loading states** keep each route's real title while data loads.

  ## CLI

  * No command or flag changed. `agentsfleet templates --json` passes `required_credentials_reasons` straight through.
</Update>

<Update label="Jun 28, 2026" tags={["What's new", "UI", "API"]}>
  ## Models and Keys is one page, and credentials say what they are

  Model and key management is a single **Models & Keys** page. The standalone Credentials page is gone — `/credentials` redirects here.

  ## What's new

  * **One Models & Keys page** — the active model shows as a hero (`model · via <credential> · LIVE`); switching sets that credential's saved model with no key re-entry.
  * **Credentials are classified server-side** — each row carries its `kind` plus non-secret provider, model, and base URL. The API key is never returned.
  * **Replace a key in place** — rotating a provider key updates only the secret and keeps model, provider, and endpoint.
  * **Custom secrets keep their own section** — `NAME=value` secrets a SKILL reads by name stay listed apart from model-provider keys.

  ## API reference

  ```
  GET   /v1/workspaces/{ws}/credentials          each row carries kind + non-secret provider/model/base_url; never api_key
  PATCH /v1/workspaces/{ws}/credentials/{name}   body {api_key} — rotate the key only, preserving provider/model/base_url
  ```

  * **`kind` is derived server-side** from the stored provider field — `provider_key`, `custom_endpoint`, or `custom_secret` — never from the user-chosen name. An unreadable legacy body lists as a custom secret and the call still returns 200.
  * **Rotate errors** — a missing credential returns `UZ-VAULT-003` (404); an empty key returns `UZ-REQ-001` (400).
</Update>

<Update label="Jun 25, 2026" tags={["Security", "Breaking"]}>
  ## The sandboxed agent runner is hardened for general availability

  ## Upgrading

  * **Egress now fails closed by default.** A runner with `RUNNER_NETWORK_POLICY` unset no longer shares the host network. To keep the previous open-by-default posture, set `RUNNER_NETWORK_POLICY=allow_all` explicitly; typos and unrecognized values also fail closed.

  ## What's new

  * **Complete secret redaction on the live tail** — the model API key and every custom secret field become their `${secrets.NAME.FIELD}` placeholder before any progress frame leaves the run, even when a secret is split across two streaming chunks. On a memory failure the frame is dropped, never sent raw.
  * **Server-Side Request Forgery (SSRF) protection for tenant hosts** — a tool dialing a tenant-configured host pins the resolved address and rejects private, loopback, and link-local targets, including the cloud metadata endpoint.
  * **Bounded retries and call deadlines** — runner backoff is capped with jitter, and a call whose watchdog cannot start fails fast.
  * **Proven kernel enforcement** — seccomp syscall traps, Landlock filesystem denial, and cgroup process and memory limits are verified against a real kernel.
</Update>

<Update label="Jun 25, 2026" tags={["What's new", "API", "UI"]}>
  ## Build the model catalogue and set the priced default from the dashboard

  Platform operators build the priced catalogue and choose the one active default model and key in the dashboard — no SQL seed, no redeploy.

  ## What's new

  * **Model catalogue** — `/admin/models` lists every priced model and adds one through a dialog (provider, model id, context cap, and per-million-token input / cached-input / output rates). Edits and deletes repopulate the in-process rate cache with no restart.
  * **Platform default** — a Platform Default card activates one model, pairing it with the key's source workspace and an optional OpenAI-compatible base URL. Exactly one default is active at a time.
  * **Catalogue-curated onboarding** — the catalogue ships empty and admins populate it.

  ## API reference

  Every route requires the `platform_admin` claim; a per-tenant admin role returns `UZ-AUTH-021`.

  ```
  GET    /v1/admin/models          list the catalogue (each row carries its uid)
  POST   /v1/admin/models          add a priced row
  PATCH  /v1/admin/models/{uid}    update a row's caps/rates
  DELETE /v1/admin/models/{uid}    remove a row
  PUT    /v1/admin/platform-keys   set the one active default
  ```

  Rows are keyed by `uid` in the URL, not `provider/model_id` — a model id can contain `/`. A create body:

  ```json theme={"theme":"vesper"}
  {
    "provider": "fireworks",
    "model_id": "accounts/fireworks/models/kimi-k2.6",
    "context_cap_tokens": 256000,
    "input_nanos_per_mtok": 950000000,
    "cached_input_nanos_per_mtok": 100000000,
    "output_nanos_per_mtok": 4000000000
  }
  ```

  `PUT /v1/admin/platform-keys` takes `{"provider", "source_workspace_id", "model", "base_url"?}`. The context cap is read from the catalogue row, never the body, and the default's `(provider, model)` must already be a priced row (rejected with `UZ-PROVIDER-004`). New codes: `UZ-PROVIDER-006` (no catalogue row for that uid), `UZ-PROVIDER-007` (the model is the active default — repoint before deleting), `UZ-PROVIDER-008` (duplicate `(provider, model_id)`).
</Update>

<Update label="Jun 24, 2026" tags={["What's new", "Bug fixes", "UI", "CLI", "API", "Security"]}>
  ## One terminal-native dashboard, and any OpenAI-compatible endpoint

  The dashboard reads as one product: one tab style, one content width, a description under each page title. Own-key model setup also drops its named-provider limit.

  ## What's new

  * **Billing** — a balance card with a full-width consumption meter and a terminal-style ledger (`date · amount · type · description`). The per-seat grid is replaced by a single "Pay as you go" row.
  * **Models and Credentials are two destinations** — `Models` (`/settings/models`) and `Credentials` (`/credentials`).
  * **A credentials vault** — `/credentials` groups model-provider keys, custom `NAME=value` secrets, and integrations. Provider keys stay write-only and masked (Replace, never reveal).
  * **Custom — OpenAI-compatible model setup** — add a credential with a base URL and an optional key, then point own-key setup at it.
  * **One install flow** — Dashboard and Fleets share one install screen, taking a template, an `owner/repo` GitHub source, or pasted `SKILL.md`.

  ## API reference

  A self-managed credential may carry an OpenAI-compatible endpoint in its stored JSON. The `PUT /v1/tenants/me/provider` body is unchanged — the URL lives in the referenced credential:

  ```json theme={"theme":"vesper"}
  {
    "provider": "openai-compatible",
    "api_key":  "<optional — omit for a keyless gateway>",
    "model":    "<model id>",
    "base_url": "https://host/v1"
  }
  ```

  * **`base_url` is validated before any run** — it must be `https` and resolve to a public host. A loopback, private, link-local, or cloud-metadata target is rejected with `UZ-PROVIDER-005` and never dialed. A `base_url` set on a named provider is rejected too.

  ## Bug fixes

  * **Custom endpoints now activate** — self-managed endpoints bill against your own provider, so they no longer require a catalogued model.
  * **Account-modal email is readable in dark mode.**
  * **Outbound model calls pin the validated address** — the non-streaming provider dial connects to the address checked at validation time instead of re-resolving, closing a DNS-rebinding gap.

  ## CLI

  * **`agentsfleet credential add`** accepts `--provider openai-compatible --base-url <url> --model <m> [--api-key <key>]` — the key is optional, the model required, and a non-`https` `--base-url` is rejected at parse time with no network call. **`agentsfleet tenant provider add --credential <name>`** then activates it. Tool secrets keep the existing `credential add <name> --data=@-` form.
</Update>

<Update label="Jun 23, 2026" tags={["What's new", "Bug fixes", "Observability"]}>
  ## Metrics export to Grafana Cloud

  `agentsfleetd` pushes metrics to Grafana Cloud, completing the OpenTelemetry triad next to traces and logs. Billing is untouched: the wallet, charge ledger, and per-event breakdowns stay in Postgres, and a metric is emitted only after its charge commits.

  ## What's new

  * **Three series** — credit drained (by posture and model), token throughput (by direction: input, cached, output), and a run-latency histogram, plus a `samples_dropped` counter reporting the exporter's own health.
  * **One credential triple** — `GRAFANA_OTLP_ENDPOINT`, `GRAFANA_OTLP_INSTANCE_ID`, and `GRAFANA_OTLP_API_KEY` enable traces, logs, and metrics together. Unset, the exporter stays off and says so in one startup log line.
  * **Collector requirement** — metrics use OpenTelemetry Protocol (OTLP) delta temporality; run an OpenTelemetry Collector with the `deltatocumulative` processor in front of Grafana Cloud Mimir.

  ## Bug fixes

  * **Traces and logs** — span names, attributes, and log bodies were emitted with a stray extra quote that produced invalid JSON, so Tempo and Loki could reject them.
</Update>
