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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@ is at the bottom; the current roadmap lives in the [README](README.md#roadmap).
- **v5.4.0 β€” The DJ stops lying, and the house gets a voice (SPEC F138–F147 Β· gh-#434, gh-#438, gh-#384, gh-#528, gh-#610, gh-#620)** β€” two epics and the sweep that cleared the deck for them. **The truth lane (F138–F142, PRs #605/#609)**: every LLM-authored line now passes a **truth gate** at the `LlmCopyWriter` seam β€” invented facts, wrong weekdays, wrong dayparts, fabricated verifiables are caught, re-asked exactly once, and template-floored if the model lies twice (the aired "Saturday morning… tonight" of gh-#438, gone); **every LLM call carries a typed cause** (`Success | Timeout | OverLength | TruthGateReject | ConnectionFailure | CanceledByWindow | EmptyCompletion | MalformedResponse`) with rolling per-model counters, so a red LLM tile finally *names* its cause and model instead of just being red (gh-#365); the **honest time signal** (F141: a 420 s budget and a "just past" variant, never an invented hour β€” gh-#526), the **boundary cadence covenant** (F142, gh-#300), and the **gap-aware crosstalk stock** (F140: runway projection + exponential backoff, gh-#546). **The model floor, measured (gh-#438, T336, #627)**: a week of those counters on the bench β€” five models, ~1.1k gated calls a day β€” put *every* model above the floor (worst share 0.3%); the answer was prompt shape, not size or family, and HARDWARE.md now says so. **The House Voice (F143–F147, PR #619 + `genwave-homeassistant` v0.1.0)**: owner announcements are a durable unit of content with a total, visible state machine (`station.announcement`, db/40) β€” `POST /api/announcements` (280 chars, 6/min, depth 12, case-folded collapse), a dedicated hashed reveal-once **announce token**, the DJ works a casual message into the next break in character or reads it **verbatim** when asked (and *always* verbatim when the flavored copy fails the gate β€” the fallback law), `aired` stamps only on the real `TrackAired`, a lifecycle guardian re-arms the claimed-but-never-aired and expires the stale, the **Announcements** admin page, and a hard **403 while the station is public** (SpectatorMode) so a public stream never carries the house's events. On the Home Assistant side: a HACS custom integration with the `genwave.announce` service, a `notify` entity, a **now-playing sensor**, and a starter blueprint gallery (dinner bell, laundry done, morning ramp) β€” gated on 2026-08-28 by a real automation on the maintainer's own HA ringing the DJ within one break cycle. **The silent-loss class (gh-#610 β†’ gh-#611/#612, PRs #615/#616)**: a catalog row the scan can never re-verify (a library discovered under a previous root) is **quarantined** after the miss grace instead of feeding the engine files that don't exist; pushes check `File.Exists` first, and a pushed-never-aired chain now WARNs β€” the canary station's 9,112 ghost rows cleared themselves two scans after boot, no SQL. **Thinking models work (gh-#620, #630)**: `Llm:ReasoningEffort` (live, default `none`, `omit` for a backend that rejects the field) rides every completions request, and a reasoning-only reply is named as such in the WARN instead of "empty after cleanup" β€” gemma4/qwen3/deepseek-r1/magistral no longer return empty copy. **The 08-20 bug bash (#596–#604)**: speakability flatten at the F68 chokepoint (gh-#541), optimistic-concurrency guard on whole-array settings writes (gh-#486, db/38), the speaking-DJ avatar enlarges on the spectator page (gh-#582), a real ESLint gate for the admin UI (gh-#553), the ffmpeg-install composite action (gh-#588), and per-run-unique compose projects for both test fixtures (gh-#569/#602). Schema: **db/38, db/39, db/40** β€” `migrate.sh` on the way up. Abstractions **5.4.0**: additive β€” `SegmentKind.Announcement` and `TimeAnnouncementFreshness` on `SegmentRequest`.
- **v5.4.1 β€” The gate learns the station's name, the sensor learns the appliance (SPEC F138.8, F144.3 amended, F145.6 Β· gh-#632)** β€” patch: the first real House Voice airing's findings, closed the same day. **The flavored lane can actually air (gh-#632)**: the truth gate treated the station's own name as an invented number β€” `GWAV 108.8` on the announcement lane read as an unsupported "108.8" β€” and refused the owner's sentence over a changed exclamation mark (`hot.` vs `HOT!`). Now the station's name is a **supported fact on every lane** (its digit runs, weekdays *and* condition words β€” a station named "Sunny 101.5" no longer trips the weather-word check; names are whitespace-collapsed before the span search; the crosstalk parser's shape checks get the same exemption), and core containment compares **word sequences** β€” hygiene-normalised, case-folded, Unicode letters and numerals with apostrophe folding, punctuation is not a word, the owner's words contiguous and in order. Case and punctuation are free; words are not: a paraphrase still falls to the verbatim floor, and attempt-one fabrication still dies. Proven through the production binary with `Station:Name` read from the host's own options β€” a station renamed "DRIFTED FM 99.9" is refused for exactly the digit run. **The now-playing sensor works on an appliance (F145.6)**: `GET /api/announcements/now-playing` moves onto its own controller with no admin-surface gate, so Home Assistant's sensor answers while `Admin:Enabled` is off (the public appliance's normal posture), public or private; submit, history and the token endpoints stay admin-surface (404), no token still fails closed (401), and the read stays off the public listener port. Mint the announce token during the temporary-admin window. **Fitness laws**: L9 (the announce-scheme fence) widens to exactly the two named carriers and reds on a third; **L10 β€” namespace cycle-freedom β€” is now a real law** reporting through `LawId.L10` over the production assemblies (a planted cycle reds), with its CONTRIBUTING row. Test infrastructure: the ephemeral-Postgres harness (`Host.Tests/Support/EphemeralStationDatabase`) and the announcement wire helpers are shared, not copied. No schema, no contract change; Abstractions 5.4.1 is byte-equivalent to 5.4.0. Filed on the way: gh-#635 (a 405 leaks admin-surface path existence with the admin plane off), gh-#636 (test hygiene). Companion: `genwave-homeassistant` README (PR #2) states the appliance posture.
- **v5.5.0 β€” The library tends itself (SPEC F150–F155 Β· gh-#529)** β€” the Library Gardener: a `BackgroundService` that houses five rot passes on a timer and a rotation signal fed by listener taste. **Five passes, one queue**: `dead_file`, `near_duplicate`, `stale_metadata`, `shelf_dust` (unaired past `Gardener:ShelfDustDays`), and `unreachable` (admitted by no weekly-grid envelope) each reconcile set-based against `library.rot_finding` (db/41) β€” no file reads of their own, dead-file evidence comes from the scanner's existing stat pass β€” surfaced on a new **Gardener page** (per-kind, near-duplicate rows grouped by pair, dismiss/eligibility/never-play/re-enrich/purge verbs) and a **dashboard tile** with live open counts. **The rotation nudge (F150)**: listeners can thumb the current track from the spectator page (`Station:Thumbs:Enabled`, a *live* setting, off by default) alongside the existing operator taste thumbs β€” both write `library.media_thumb` under the same `ThumbSource`-tagged row, decayed by half-life (`Gardener:HalfLifeDays`) and saturation (`Gardener:Saturation`) into a `[-1, 1]` nudge on `library.media_rotation` that adds into the persona ranker's rung-0 score (`Gardener:NudgeGain`). **Deep Cuts (F152)**: a show's own rotation predicate (`MaxPlays`/`NotAiredWithinDays`, `PUT /api/shows/{id}`) filters candidates by construction, ahead of the ordinary F81.6 envelope ladder, with its own **R0β†’R3 relax ladder** so a thin catalog under a tight predicate never starves a boundary instead of straddling it. **File actions are opt-in (F154)**: retag/rename/move β€” no delete verb β€” behind `Gardener:FileActions:Enabled` (default off) *and* the new `compose.fileactions.yaml` overlay (widens only the api's own `/media` mount to `:rw`); every write is jailed to `MediaRoot`, never overwrites, and a retag swaps in behind a same-directory `.gwbak` backup of the original β€” a failed revert leaves that backup as the only surviving original, and further retags on the file refuse until an operator resolves it. Schema: **db/41** (`library.media_rotation`, `library.media_thumb`, `library.rot_finding`, `library.file_action`, the `rot_kind`/`rot_state` enums) β€” `migrate.sh` on the way up. Abstractions **5.5.0** is a **minor** bump: `ScheduleSegment.EffectiveEnvelope` is additive, extracted so the Gardener's own `unreachable` pass and `ScheduleResolver` share byte-identically the same per-field envelope-fallback formula instead of two independent copies drifting apart. Filed on the way: gh-#649 (dev-box test fixtures can exhaust Docker's default address pools under a full-parallel `dotnet test` run β€” CI unaffected, runs per-project), gh-#650 (a directory symlink inside `MediaRoot` aliasing another directory makes the scan double-count one file as a ghost `discovered` row; the file-action planner already refuses moves into a symlinked target, the scan-side dedupe is the open half).
- **v5.5.1 β€” The queue at scale (gh-#654/#655/#657 Β· SPEC F153.9/F153.10 riders)** β€” patch: the Gardener's first contact with a real library (the demo's 1,532 dead-file corpses) proved the findings page's one flat 1000-row fetch a starvation defect β€” ordered by kind with `dead_file` declared first, it consumed the whole page and rendered "Showing first 0 of 233" on every other section (gh-#654). The page rebuilds on the catalog idiom: server-rendered `?tab=&page=&limit=`, five badged kind tabs, "Page N of M" pagination with a 25/50/100/250 size picker (default 25, URL-only), verbs refreshing the server truth via `router.refresh()`. **Near-duplicates page by whole group** β€” `limit`/`offset` count distinct clusters and a page can never split one β€” with member semantics the review loop proved from both directions on live Postgres: a **dismissed** member still renders with its cluster (it is still a duplicate; hiding it let *Keep this one* leave the hidden sibling eligible), while a **resolved** member never does (the resolve keeps its stale `group_key`, and rendering it let *Keep this one* bulk-ineligible a track that is no longer a duplicate). Kind-scoped `GET /api/gardener/findings` now carries an exact `total` (groups for near-duplicates, rows otherwise); un-scoped callers keep the old response byte-for-byte. The purge trigger reads **"Purge dead tracks…"** β€” a verb, not a status (gh-#655). Page numbers clamp inside Int32 so paging params stay hints, never 400s, and the pager becomes one shared component (the catalog page converts to it, markup byte-identical). Left open on purpose: gh-#656 β€” the purge tripwire still refuses a legitimate whole-library-replacement cleanup; its typed-confirmation override gets its own design.
10 changes: 5 additions & 5 deletions compose.pinned.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@

services:
admin_ui:
image: ghcr.io/genwave-org/genwave-admin-ui:home-v5.5.0 # bump per release
image: ghcr.io/genwave-org/genwave-admin-ui:home-v5.5.1 # bump per release
pull_policy: always
# `!reset` drops the base `build:` context entirely β€” see the file header for why.
build: !reset null

engine:
image: ghcr.io/genwave-org/genwave-engine:home-v5.5.0 # bump per release
image: ghcr.io/genwave-org/genwave-engine:home-v5.5.1 # bump per release
pull_policy: always
# Same !reset rationale as admin_ui above (gh-#93). Note genwave.liq is a bind mount
# from the checkout (see compose.yaml), NOT baked into this image β€” the image carries
Expand All @@ -44,15 +44,15 @@ services:
# happens at the FIRST TAG CUT AFTER gh-#240/#241 β€” until that release's pin bump this
# tag does not exist and the pull fails loudly by design. Bump with the other four pins
# per release.
image: ghcr.io/genwave-org/genwave-piper:home-v5.5.0 # bump per release
image: ghcr.io/genwave-org/genwave-piper:home-v5.5.1 # bump per release
pull_policy: always
# Same !reset rationale as admin_ui above (gh-#93).
build: !reset null

icecast:
image: ghcr.io/genwave-org/genwave-icecast:home-v5.5.0 # bump per release
image: ghcr.io/genwave-org/genwave-icecast:home-v5.5.1 # bump per release
pull_policy: always

api:
image: ghcr.io/genwave-org/genwave:home-v5.5.0 # bump per release
image: ghcr.io/genwave-org/genwave:home-v5.5.1 # bump per release
pull_policy: always
Loading