# Changelog All notable changes to `dsh-all-usage` are documented here. ## [1.1.17] - 2026-10-01 ### Fixed - **Writes work in the DSH desktop client again.** The write routes (price save, immediate sync, holiday fetch, alias) required an `Origin` header equal to the loopback host. The desktop shell serves the UI on its own scheme and forwards page requests through Electron, which strips `Origin` along with `Host`, `Cookie` and `Sec-Fetch-*`, so every desktop write was rejected with 403: saving prices reported "no permission", the models.dev catalog could never be synced, and the auto-sync checkbox rolled back. Writes are now authorized by the process capability alone — an unguessable secret that a cross-origin page can neither read (the read routes send no CORS headers) nor attach to a form POST — while a present `Origin` must still be loopback or the shell's own scheme, and `Host` must still be loopback so a rebound DNS name cannot reach the API. - **A missing `usageBacked` no longer reads as a verdict.** A host that predates the merged price table sends no `usageBacked`, and the panel rendered that as "no ledger usage" even for rows with recorded calls; it now degrades to "usage unknown" (distinct flag, row style and footer count), and says so when the payload carries no model list at all. - **A rotated write capability heals itself.** The status poll now carries a derived `capabilityId` (never the capability itself), the page treats a rotation as a full refresh, and a write rejected with 403 re-reads the capability from the full snapshot and retries once before reporting anything; the message now names a stale capability instead of claiming a permission problem. ### Added - `GET /api/all-usage/status` reports the running plugin version and the derived capability id, so a page served by a newer package than the running host can say "host plugin vX / this page vY differ — restart DSH" instead of leaving that mismatch invisible. ## [1.1.16] - 2026-09-29 ### Added - **DSH 0.2.0-rc.2 is a supported runtime.** The real Cordis smoke passes on it — every route, the session hooks, the ledger flush, the revision fast path and disposal — so `dsh.compatibility.runtime` gains `>=0.2.0-rc.2 <0.2.1-0`, `0.2.0-rc.2` joins the verified list (now six runtimes) and the CI smoke matrix (Node 22/24, twelve jobs). The line keeps the 0.1.7 Cordis pairing (cordis 4.0.4 / loader 1.0.5 / timer 1.1.6) and the abstract settings seam instead of the file-backed provider, which is exactly what the smoke runs against. README (both languages, including the badge and the verified matrix) follows the new declaration. ## [1.1.15] - 2026-09-28 ### Added - **Chinese statutory holidays can price as a full off-peak day.** A peak plan now carries an explicit `holidays` date list (China Standard Time calendar days: validated, sorted, de-duplicated, at most 400 entries, and part of the policy hash so the existing audit/repricing rules apply unchanged). On those days no rule can claim an instant, and each cost snapshot records `pricingHoliday`, so the ledger distinguishes a holiday off-peak band from an ordinary one. The panel exposes it as a switch plus a date list that also accepts `start..end` ranges, and `POST /api/all-usage/pricing/holidays` fetches one year of the State Council arrangement (the holiday-cn dataset: jsDelivr first, GitHub raw as a fallback, 24-hour host-side cache) so the list can be filled with one click; the fetch writes nothing — only saving freezes the dates (plus a provenance note that stays out of the policy hash) into the plan. The built-in DeepSeek peak plan now ships the 2026 arrangement itself (33 off-days taken from the State Council notice, with the provenance stored next to the dates), because DeepSeek's own rule is "Monday to Friday excluding Chinese public holidays": first-party and mapped DeepSeek routes therefore price holidays as off-peak with no configuration at all, and snapshots priced under the previous plan are migrated once. The official wording keeps weekends off-peak in full, so swapped-in working weekends (调休) need no extra handling. ### Changed - **Cost Settings is now one editable price table.** The separate "Model mappings" and "Explicit price overrides" sections are gone: every ledger model gets a row whose four price boxes write that model's manual price, whose official-model column sets or clears the identity mapping (and previews the target rates immediately), and whose two chips expand a context-rate-band editor and a UTC peak/off-peak editor. Rows that exist only in the saved configuration (an override or mapping with no ledger usage yet) are returned with `usageBacked: false` so they stay visible, editable and removable. - **Peak/off-peak plans are editable per model.** `GET /api/all-usage/pricing` now carries `temporalSchedules` (deduplicated like `tierSchedules`) plus `temporalExplicit`, `temporalBuiltin` and `temporalConfigInvalid` on every row, so the panel can show whether a row uses the built-in DeepSeek table or a custom plan, copy the built-in table for customisation, restore it, or drop the plan. Changing a plan reconciles that model's history against the new policy (the documented audit semantics); price edits still never rewrite existing positive costs, and the panel says so. ### Fixed - The pricing configuration round-trips `providerId` and `temporalPricing` on overrides. Dropping `providerId` from the snapshot turned a provider-scoped manual price into a provider-agnostic one on the next save and stopped it from being found as a mapping target, so a saved mapping could silently fall back to the catalog price. - `GET /api/all-usage/pricing/models` returns `rates`, `providerName`, `tiered`/`tierCount` and `builtinTemporal` per match, so the mapping column can preview a price before saving. ### Internal - The compact `/api/all-usage` snapshot keeps its shape: temporal detail, schedule ids and configuration-only rows are only emitted for the detailed pricing payload, and `test/pricing-editor.test.js` guards that boundary. ## [1.1.14] - 2026-09-27 ### Fixed - **`deepseek-flash` gets the DeepSeek peak/off-peak plan.** models.dev lists it (DeepSeek V4.1 Flash) in the same `deepseek-flash` family and at the same standard rates as `deepseek-v4-flash`, but the built-in table only carried the v4 ids, so every record for that model was priced flat and the cost panel reported `temporalExemptReason: "no-temporal-profile"` — a peak-hour request was under-reported by roughly 57%. The table now declares the model; the off-peak rates still come from the live catalog entry, and the plan still applies to first-party routes only. Records already stored with that exempt verdict are migrated once against their own usage instant by `reconcileTemporalPricing()` and written back to the ledger, so existing history is corrected on the first start after upgrading. Reported by @baileyh8 in #2. ## [1.1.13] - 2026-09-25 ### Added - **DSH 0.1.7-rc.2 is a supported runtime.** The real Cordis smoke passes on it — every route, the session hooks, the ledger flush, the revision fast path and disposal — so `dsh.compatibility.runtime` gains `>=0.1.7-rc.2 <0.1.8-0`, and `0.1.7-rc.2` joins the verified list and the CI smoke matrix. The DSH desktop client runs this runtime line, so the caption-strip work from v1.1.11/v1.1.12 is covered by the same declaration. ### Changed - The runtime smoke is version-aware about settings. 0.1.7 replaced the file-backed `@deepseek-ai/dsh-settings-file` provider with an abstract settings seam that no runtime package instantiates; the smoke runs without a settings service on that line and asserts that the plugin stays healthy without one, which is the behaviour the plugin already implements (`ctx.settings` is optional). - `0.1.5-rc.2` is covered by the CI smoke matrix instead of being verified locally only, so the verification matrix has no exception left. - `scripts/check-package.mjs` now fails when a runtime is called verified without appearing in both the CI matrix and the smoke profile table, so a declaration can no longer outrun its coverage. ## [1.1.12] - 2026-09-25 ### Fixed - **The runtime smoke test no longer counts routes by hand.** It asserted that dispose removed exactly 9 exact routes, so v1.1.11 — which adds `/api/all-usage/client-env` — failed every Cordis smoke job and, with it, its own npm publication, while the published package itself was correct. The expected count is now derived from the route list the test already checks, so adding a route can no longer leave a stale number behind. ### Note - v1.1.11 was published to GitHub Releases but never reached npm: the publish workflow checks out the release tag and verifies it, and a published tag is never moved. The desktop caption-strip fix reaches npm users with this release; the shipped runtime code is identical to v1.1.11. ## [1.1.11] - 2026-09-25 ### Fixed - **The desktop client's own close button is no longer covered.** The usage sheet is a full-window overlay, so its backdrop and grabber bar were painted straight across the 40px caption strip the DSH desktop client keeps for its window buttons (minimise / maximise / close), dimming the client's close button behind the panel. When the host marks the document with `data-windows-titlebar`, the sheet now starts below `--dsh-windows-titlebar-height` (40px on Windows), so the strip stays clear. Browsers are unaffected: without the marker nothing changes. - **The strip reservation never actually applied.** `Number(localStorage.getItem('dsh-all-usage:topInset'))` reads an unset key as `0`, which passed the `>= 0` check and returned early — the host marker, the Window Controls Overlay geometry and the user-agent fallback were dead code, so the panel reserved nothing on any desktop host even though the logic was in place. An unset key is now distinguished from an explicit `0`. ### Changed - The reserved strip is taken from the host's own declaration first (`data-windows-titlebar` plus `--dsh-windows-titlebar-height`), then the Window Controls Overlay rectangle, then the user agent. `localStorage['dsh-all-usage:topInset']` still overrides everything, so the strip can be tuned without a rebuild. - The client reports its environment through the new `GET /api/all-usage/client-env` route, surfaced as `clientEnv` in `/api/all-usage/status`: user agent, overlay height, the reserved strip **and where that number came from**, viewport, and the panel's own rectangles. It reports at load and again whenever the sheet opens — this is what exposed the reservation bug from outside the desktop app. ## [1.1.10] - 2026-09-14 ### Fixed - **The ledger fast path is back.** DSH 0.1.5 renamed `sessionPersistence.listSnapshots()` to `list()`, so the plugin never obtained a per-session revision: `sessionsSkippedByRevision` stayed at 0 and every baseline re-read every session log (431 reads / ~13 minutes for 479 sessions on the maintainer's host). Both spellings are now accepted; against a real 0.1.5 service the plugin reports `persistenceSnapshotsAvailable: true` and skips unchanged sessions again. - **A registry change during a baseline is no longer dropped.** `synchronizeWorkspaceRegistry()` returned early while `scan.done` was false, so a workspace added during the (previously very long) scan stayed invisible until the next restart. Such a change is now remembered and replayed as soon as the scan settles. ### Changed - The workspace registry is additionally polled every 30 seconds, so a missed `domain/changed` event can no longer leave the workspace list stale until a restart. - `/api/all-usage/status` exposes `workspaceProbeEvents`, `workspaceSyncRuns`, `workspaceSyncAdded`, `workspaceSyncRemoved` and `workspaceSyncDeferred` (cumulative, not reset by a baseline) so the probe can be verified on a live host. ### Note - The first start after upgrading still reads every log once: ledger records written before this release carry no `lastRevision`, so the revision comparison has nothing to match against. From the next start on, unchanged sessions are applied from the ledger without re-reading their logs. ## [1.1.9] - 2026-09-12 ### Fixed - Deleting a workspace no longer erases its recorded usage. The registry probe used to call `removeSession()` for every session of a deregistered workspace and drop their ledger rows, and recovery skipped any row whose `sourceCwd` no longer mapped to a registered workspace; together they destroyed history the durable ledger had already recorded. (v1.1.6 fixed a different path — the DSH 0.1.5 `session.events` removal that wrote empty ledger records.) ### Changed - Usage from every removed workspace is now summed into a single **Deleted** row instead of vanishing, and one row per dead workspace is no longer created. Retiring a workspace re-keys the live aggregate in place through the new `aggregation.retargetSession()`, so usage folded from the live feed that had not reached the ledger yet survives too. - Persisted ledger rows keep their original workspace id and cwd: retargeting happens on the in-memory copy only, the bucket is re-derived from the ledger after every restart, and a row with no usage never advertises the bucket. - The strict registration boundary is unchanged — sessions in unregistered or deleted directories are still excluded from new statistics — and legacy `unregistered:` rows are still never resurrected. Re-registering a workspace keeps its old history in the Deleted row while new sessions count under the live workspace. ## [1.1.8] - 2026-09-11 ### Fixed - README.md still quoted the pre-0.1.5 compatibility declaration (`>=0.1.1-rc.1 <0.1.2`) and a verification matrix that stopped at 0.1.1, contradicting the shipped `package.json` after v1.1.6 and v1.1.7. Both language sections now record the corrected per-tuple range and list `0.1.5-rc.1` in the verified matrix. ### Changed - The CI runtime smoke matrix now covers `0.1.5-rc.1` alongside `0.1.1-rc.2` and `0.1.1-rc.1`, and selects the Cordis/loader/timer line per runtime version (0.1.5-rc.1 ships cordis 4.0.2 / loader 1.0.3 / timer 1.1.4) instead of pinning a single hard-coded set. ## [1.1.7] - 2026-09-11 ### Fixed - The declared DSH runtime compatibility range now carries an explicit prerelease branch per tuple (`>=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0`). node-semver only lets a prerelease version satisfy a range when a comparator shares its `major.minor.patch` tuple and itself carries a prerelease tag, so the previous broad-looking `>=0.1.1-rc.1 <0.1.6` silently reported `0.1.5-rc.1` — the runtime this release targets — as unsupported. ## [1.1.6] - 2026-09-11 ### Fixed - DSH 0.1.5 compatibility: that runtime replaced the Session `events` getter with `snapshotEvents()`, so the live flush path read an empty event log and folded every flushed session into an empty ledger record (`lastSeq: -1`, no turns, no usage). The dashboard stayed correct because live totals come from the `session/event` hook, but the durable "usage survives session deletion" guarantee silently degraded and restart lost the zero-rescan fast path. Event access now prefers `session.events` and falls back to `session.snapshotEvents()`, leaving 0.1.1 behaviour unchanged. - The runtime smoke test now asserts the persisted ledger keeps the flushed session's sequence and usage, and covers 0.1.5-rc.1 alongside the 0.1.1 line, so this regression cannot return unnoticed. ### Changed - Declared DSH runtime compatibility widened to `>=0.1.1-rc.1 <0.1.6`, with `0.1.5-rc.1` added to the verified list; the runtime smoke test pins the Cordis/loader/timer lines per runtime version. ## [1.1.5] - 2026-09-07 ### Added - Workspace registries are now kept fresh by an automatic **probe**: all-usage listens to DSH's `domain/changed` event for the workspace domain and, on any durable registry write (create/delete/rename/reorder/archive/attach/detach), rereads `workspaceRegistry.list()`. The sync is incremental — only added workspaces are scanned (the sessions that belong to them) and removed workspaces have their sessions subtracted via inverse folding, while unchanged workspaces reuse their computed aggregates and ledger with zero rescan. The manual Refresh workspaces control, its `POST /api/all-usage/workspaces/refresh` route, and the refresh-workspaces client button were removed. Usage remains intentionally restricted to registered workspaces; unregistered cwds are ignored. ### Fixed - Usage is strictly limited to DSH-registered workspaces: sessions whose cwd does not map to `workspaceRegistry.list()` (including existing directories absent from the registry) are ignored, and legacy `unregistered:` ledger rows are never resurrected as history. - Ledger rows whose persisted `sourceCwd` no longer maps to a registered workspace (for example the session directory was removed) are skipped during recovery instead of being restored as stale data; the workspace source path is now persisted with every ledger record so restart recovery can validate ownership. ## [1.1.4] - 2026-09-03 ### Added - DeepSeek peak/off-peak billing as a first-class, versioned **temporal pricing plan**: the cost schema is v2, and each cost snapshot records the billing instant, its time source (request-context / usage-event), the UTC band (peak / off-peak), the policy id, and a policy hash. Request logs show the band and UTC billing time, and the cost settings panel marks which usage models follow the DeepSeek band plan. - Built-in first-party DeepSeek profiles (deepseek-v4-flash, deepseek-v4-flash-vision-exp, deepseek-v4-pro) with explicit per-band rates for the UTC windows 01:00-04:00 and 06:00-10:00 on weekdays; the peak rates are stored as data, never derived as an automatic discount rule. - UTC band boundaries are half-open: 00:59:59 off-peak, 01:00:00 peak, 03:59:59 peak, 04:00:00 off-peak, 06:00:00 peak, 10:00:00 off-peak; weekends are off-peak. Band selection uses UTC fields only, so viewer timezones, DST, and fractional-hour zones cannot change a result. - Deterministic reuse: a later usage sample for the same turn/step reuses the previous cost snapshot only when identity, token buckets, band, policy id/hash, and applicability all match, so a message crossing a peak boundary never inherits the chunk price. - Requests preferred as the billing instant when the request/context time matches the usage turn/step; otherwise the usage event time is the auditable fallback. Both are persisted in the snapshot. - Controlled DeepSeek temporal reconciliation: first-party DeepSeek snapshots are re-priced against the usage instant on baseline/backfill/sync, v1 snapshots are migrated to the v2 shape in memory and in the persisted ledger, and a plan whose effective window does not cover the instant fails closed as unsupported (temporal-price-history-unavailable) instead of guessing. - Route safety boundary: the band plan applies only to first-party deepseek routes or routes explicitly mapped to the DeepSeek official entry; reseller routes (OpenRouter and other gateways) keep the static official price and are labelled route-not-official. - Respect the official effective instant: the built-in DeepSeek band plan starts exactly at 2026-08-16T16:00:00Z (V4-Flash-Vision-Exp at 2026-08-21), and usage before it fails closed as unsupported instead of inheriting today's V4 rates. - Persist the request-context archive inside ledger records and seed incremental folds from it after a restart, so an open request whose context arrived in a previous batch keeps its peak/off-peak billing instant. - Clear stale request-context instants and route identity on full folds (baseline rebuilds and live resyncs), so a history rewrite that removes a request no longer leaves its band attached to later usage. - Keep priced v2 snapshots auditable across catalog refreshes: the policy hash no longer includes live entry rates and reconciliation never rewrites priced history; an explicit repriceTemporal=true request re-estimates every snapshot. - Support an ordered policy archive (policies: [...]) with non-overlapping effective windows, reject overlapping rules across rules (JSON array order can never decide a price), keep an invalid temporal config visible and fail closed instead of silently falling back to the built-in profile, and compare temporal plans in the duplicate-candidate check so conflicting band plans resolve ambiguous. - Include pricingAt, pricingTimeSource, band, policy id and policy hash in the reconciliation equivalence check and in the CSV audit export. - Persist the invalid temporal-config sentinel through the serialize/normalize cycle: a rejected temporalPricing edit stays fail-closed across restarts instead of re-enabling the built-in DeepSeek profile. - Rebuild a session atomically on authoritative live resyncs: usage samples, turn records, date indexes and cost aggregates are removed before the snapshot is folded, so usage deleted by a rewritten history disappears from the aggregate (lagging snapshots keep upsert semantics until the follow-up resync aligns). - Base the live resync cursor on the snapshot tail (merged with an absorbed fallback event) instead of the previous cursor, so truncated or rewritten history cannot skip over missing events that arrive later. - Accept deepseek-official as a first-party catalog provider in the official provider rules, so its entries price and follow the band plan like deepseek. - Validate that a priced snapshot's instant is still covered and band-consistent when deciding whether a policy still matches: snapshots that slipped into a gap (same id/hash, window moved) fail closed instead of staying priced. - Include pricingAt and pricingTimeSource in the live/incremental reuse check, so audit metadata refreshes when the request instant or its source changes even within one band. - Use real LRU eviction (delete + set) for the bounded request-context archive, applying the same 512-entry cap to the live fold, the persisted ledger, and deserialization. - An authoritative live resync (full snapshot or a snapshot that provably contains the triggering event with a monotonic sequence) rebuilds the session's ledger record from the snapshot, replaces it in state and persists it, so a restart after a live history rewrite cannot resurrect deleted usage; persist failures keep explicit dirty/recovery state and surface as resync-ledger-persist-failed. - Live resync authority is now proof-based: snapshots that only have a higher tail, or events without a usable sequence, are treated as lagging (upsert the event, stay pending, align on the follow-up resync) instead of being destructively replaced. - Show real vendor brand icons instead of neutral dots for models in the request log, the selected-call details, the model summary table, and the cost-settings match table. The 11 brand SVGs live in assets/model-icons/ with a manifest (provider aliases, model prefixes, exact overrides, source and licence) and are validated and embedded as data: URIs by scripts/build-client.mjs, so the runtime never reads local paths or the network. Model namespaces win over the DSH provider name so reseller and gateway routes are attributed by the model they actually served; unknown or mixed-brand rows keep the neutral fallback, and workspace colour dots plus chart legend dots are unchanged. - Harden the model brand icons: the load-failure flag is scoped to one icon identity (a shared component that later shows another brand retries instead of staying neutral), an unrecognised actualModel no longer inherits the requested model's brand, model prefixes match on token boundaries only (o10-preview and o3x stay neutral), the resolution cache is bounded, and labelled icons expose the brand through the image's accessible name instead of a title on an aria-hidden host. - Build-time SVG validation now decodes XML entities before checking, rejects unquoted URI attributes, external CSS url()/image-set() and @import targets (including escaped/commented/CDO-CDC/namespaced forms), DOCTYPE/CDATA/processing instructions and every external-resource element; the CSS checks run only on style attributes and