Rendered from the repo-root
CHANGELOG.md.
Changelog¶
All notable changes to Orpheus are recorded here. The format follows Keep a Changelog.
For a reader-facing tour of what changed and how to turn each thing on, see
What's new. Individual components carry their own
CHANGELOG.md and VERSION alongside their pyproject.toml; this file is the
repository-wide record.
[0.4.0] - 2026-09-01¶
The summer update. Everything here is additive and backwards compatible: a previous release reads a database this one has opened, and a configuration file written for the previous release parses unchanged.
Added¶
Messaging backplane¶
- NATS + JetStream as the default broker, installed and supervised by the
orpheus-backplaneservice (make install-backbone). One binary provides pub/sub, durable streams, and a key/value store. - An
EventBusabstraction withcreate_event_bus(); no component constructs a broker client directly. Mosquitto remains a one-line fallback viaevent_bus.backend: "mqtt". - JetStream surfaces on the bus — request/reply, key/value, and durable streams — plus NATS parity for the MQTT behaviors that had no direct analogue: key/value-TTL presence in place of last-will, and retain-as-last-value.
- Cold-broker resilience. An agent started before the broker is up begins
disconnected and attaches when the broker appears, rather than crash-looping.
event_bus.connect_required: truerestores hard-fail behavior; the default is derived from whether the broker URL is loopback. - Multi-host deployment.
make install-hostandmake install-backboneinstall a per-host subset; bus-client units read/opt/orpheus/config/.envfor a per-host broker URL; opening a non-loopback listener requires an auth file. - Configuration over the bus — off by default.
config_service.enabledreads shared configuration from the broker's key/value store, so multi-host installs stop copying identical YAML.orpheus-config pushis the single write authority.
Agent substrate¶
- An
Actorbase composing the agent lifecycle — bus connection, subscriptions, heartbeat, signal handling, shutdown, statistics — withon_started/on_stoppinghooks. The crow, correlator, bird, and audio-events agents run on it. - An injectable
ClockandPeriodicTask, so tick-driven behavior is testable without wall-clock waits. - Per-instance identity.
ORPHEUS_AGENT_INSTANCE_IDfrom the deployment environment lets one agent run as several instances with no per-agent code. - Per-agent tick rates. The optional
agents:block tunes the heartbeat cadence of any agent built on the actor base — every one except the video snapshotter and video timelapser, which have no heartbeat.
Detection and correlation¶
- An audio-events agent (PANNs) covering the whole AudioSet ontology alongside the bird specialists, with bounded concurrent clip processing and a shared GPU-or-CPU device policy.
- Cross-classifier identity. Detections carry a chain root, so several classifiers describing one animal produce one entity with all the evidence attached, correlated by shared source rather than by co-occurrence.
- An entity-type taxonomy — a data-driven registry deriving a coarse path
such as
Animal.Bird.Crow, an additiveentity_typecolumn, a migration for legacy rows, and off-by-default per-type bus topics. - Automatic equivalence discovery — on by default. A background scan proposes that one classifier's label and another's mean the same animal, auto-accepts above a threshold, and queues the rest for review. Guarded against proposing equivalences within a single namespace.
- Corollary discharge — off by default. The playback agent publishes
playback windows; entities overlapping them are tagged
is_self_generatedrather than dropped, so the system does not record itself as wildlife. - Late-arrival enrichment — off by default. A classifier reporting after its cluster closed attaches to the right entity instead of creating a duplicate.
- Latent state-space memory — off by default, consumer stubbed. The correlator records what was present at this site at this hour; nothing reads it back yet.
- A geographic plausibility filter for BirdNET, with a site whitelist and a soft-admit path so a rare-but-real visitor can still surface.
- Weather-station ingestion — stubbed. The ingestor, database table, correlator context join, and dashboard card exist; the Ecowitt field mapping is unimplemented, so the ingestor exits on its first poll.
Dashboard¶
- New pages: Entities, Audio Events, Equivalences, and Diagnostics.
- Diagnostics shows correlator health including chain-completion latency, a per-volume storage trend with a days-until-full projection, a cross-agent error feed, agent presence, audio system health, and a service log viewer.
- External species links to iNaturalist, Wikipedia, GBIF, and AudioSet.
- A graceful expired-clip state for evidence whose media has rolled off retention, instead of a broken player.
- Time-of-day filtering, pagination, and a refetch indicator across Birds, Crows, Audio Events, and Entities, with filter state in the URL so a view is bookmarkable.
- Entity-type chips and self-generated tags on entity rows.
- One-click guest sign-in via a server-side endpoint, gated by
ui.guest_quick_login(default on). - Request-timing middleware for slow-query visibility.
Operations¶
make verify-deploy— a read-only post-upgrade check covering component installs, service state, models, root-filesystem headroom, log bounds, and that the database still reads.orpheus-storage-sweep— one component that owns every deletion under the data root, run as a one-shot from a 15-minute systemd timer. Per-category ceilings (max_gb) are enforced regardless of free space; belowstorage.retention.reserve_gbevery category above its floor gives up data in proportion to what it has to give;floor_daysandmin_file_age_hoursare never breached, and a conflict between a floor and a ceiling logs CRITICAL rather than deleting recent recordings.make storage-reportis a dry run, the first sweep after an install is report-only for 24 hours, and every sweep that deletes writes a CSV manifest of exactly which files went.- Host log bounds. Every shipped unit carries a log rate limit
(
LogRateLimitBurst=500/30s against systemd's default of 10,000) and aSyslogIdentifier, so one stuck service cannot fill the disk and an operator can tell which one is shouting. Container logs are capped per service, andmake dev-stacktrims its own log files.make install-log-boundsopts into a persistent, capped journal with the syslog mirror off. - Deployment manifests from configuration.
make manifestsgenerates systemd or docker-compose topology fromorpheus.yaml. - Model provisioning —
make check-modelsandmake download-models. - One-time backfills for
root_event_idandentity_type, both with--dry-runvariants. - Independent per-component versioning — a
VERSIONfile per component read dynamically at build time, withscripts/bump-version.sh. - Runbooks for laptop verification, Jetson rollout and rollback, distributed deployment, and the what's-new feature tour.
Observability¶
- Health over key/value — off by default.
event_bus.health_kv_enabledmirrors agent health into a KV plane andui.health_sourceselects which plane the dashboard reads, with a diff oracle for comparing them during migration. - Agent presence — off by default. Agents announce themselves continuously from the heartbeat; the dashboard shows who is alive now.
- A durable event log — off by default.
event_sourcing.shadow_publish_enabledalso writes detections to a bounded JetStream stream, withorpheus-reconcileto compare stream against database. SQLite remains the source of truth. - An OpenTelemetry tracing foundation — stubbed. The module and optional dependencies ship and the configuration is accepted, but nothing is instrumented, so enabling it emits nothing.
- Per-volume storage history with a fill-rate projection.
Sharing and portal¶
- A read-only
DetectionDBmode and a read-only-replica dashboard, so heavy reads can be served away from the station. orpheus-mirror— a consistent database snapshot (VACUUM INTO) and push-only transport, as a CLI. No service unit ships, so scheduling it is your own supervisor's job.- A public projection and export — off by default. A privacy chokepoint that buckets time to the day and reduces location to a site label, never a coordinate, with a provenance envelope on the export.
- API rate limiting and per-query time budgets — off by default, for a UI exposed beyond a LAN.
Testing and development¶
- The Simulacrum — the whole collective in containers, with a synthetic
source, a replay mode that plays real clips through the system, a real-model
fleet profile, per-service health checks, and
make sim-validateasserting a real clip drives the full cascade to an entity. - A generated failure matrix over agent-down, topology, and multiplicity scenarios, driven from a contract oracle.
- An end-to-end BDD suite (
make test-bdd) and shell tests (make test-bash). - Historical event replay (
orpheus-replay) for detections and entities. make dev-stack— every component as a background process on a laptop, withdev-status,dev-logs,dev-stop.- A documentation site built from
docs/, published from the public repository, withmake docs-previewserving the built site at the path it is published under. - Repository guardrails (
make guardrails) that fail the build when an agent is half-wired, a page is missing from the site navigation, or the README's roadmap counts drift from the ledger. - A dry run for the issue-board sync, so a run can be rehearsed before it touches a public board.
Changed¶
- Schema migrations are additive and run on first open, so a previous release still reads a database a newer one has opened. Compound and covering indices are built on first open of an existing database; on a large history this pauses startup and says so.
- Agents no longer delete recordings. The
_periodic_cleanuptasks in audio-motion and video-motion and the inline age purge in video-snapshotter are removed;orpheus-storage-sweepcovers those directories and the timelapse directory that nothing cleaned before. The Diagnostics storage panel reads the sweep's published report instead of the agents' health payloads.raw_audio_days,raw_video_days,max_size_gb,cleanup_trigger_percent,cleanup_amount_percent,check_interval_hoursandvideo_snapshotter.retention_daysare still accepted and validated so an existing configuration parses, but nothing applies them.detections_daysfor database rows remains unapplied. - The Entities page is served by a covering index and samples its scatter server-side, and the diagnostics history endpoints aggregate from raw rows rather than building a model per row. Measured on a station holding 2.7 million detections: Entities 27.5s → 5.8s warm, bird-correlation 39.7s → 3.7s warm.
make update-servicesstops on the first failure instead of reporting success, and covers every installed component rather than a subset.orpheus-mqttis noworpheus-backplane, broker-agnostic; the unit keeps anorpheus-mqttalias so an existing install is not orphaned.make test-allincludes every component and exits non-zero when any suite fails.make dev-stopstops the whole process tree, not just the launch wrapper that spawned it.- The accounts database follows
ORPHEUS_DATA_ROOTinstead of probing hardcoded paths; an explicitORPHEUS_UI_DATABASE_URLstill wins, and an existing database is never orphaned. ruffis pinned exactly rather than floored, so a linter release cannot turn an unchanged tree red.- Documentation is organized by what a reader came to do, published as a site, with a comparison page, a security posture page, an AI-assisted install path, and a stated voice and audience for contributors.
Fixed¶
- BirdNET used softmax on multi-label logits, collapsing simultaneous species to one; it now uses sigmoid and de-duplicates windows by label index.
- Opening a pre-
root_event_iddatabase crashed — an upgrade blocker. - Correlator health tolerates an un-migrated or read-only database instead of erroring.
- Deployment suppressed dependency resolution, so a new platform dependency
never reached existing component environments; the platform installer also
omitted its
VERSION, so/optbuilds carried0.0.0. orpheus-gpsand the UI backend did not self-install, so a deployed environment could not import them.- Schema initialization now waits out a concurrent migration rather than failing after five seconds — on a large history the loser of that race was being restarted by systemd.
- Audio-events dropped every corvid detection because of a whitelist missing eight bird-like AudioSet labels, and did not survive a missing GPU.
- Auto-discovery could propose same-namespace equivalences and its test rotted against the wall clock.
- The correlator gated clip-interval overlap on shared clip origin, and its expiry failures are now counted and survivable rather than silently killing the timer.
- UI failures surfaced as errors rather than blank charts, the
dashboard.poll_intervalknob actually takes effect, a reset during compute no longer clobbers the bird-like cache, and a missing replica snapshot falls back to the live database with a warning. - Memory-bounded streaming replaced materializing large windows in auto-discovery, reconciliation, and equivalence diagnosis.
- The NATS bus no longer leaves a zombie worker after an aborted connect, reports transport-down as a connection error rather than an attribute error, and its teardown outlives the drain.
make sim-validatecould report success while proving nothing — it passed with the classifiers stopped, and exited zero with nothing running at all.make check-modelsreported no models on a fully provisioned station, because it only looked for two file extensions, and named a download command that does not exist.- The end-to-end suite tested a stale bundle, because it did not rebuild the frontend it drove.
Security¶
- Unauthenticated arbitrary file read via the single-page-app route. The
catch-all joined a raw URL path onto the static directory with no containment,
so a bare
GETcould read any file the service user could open — including the environment file holding the session-signing secret. - Arbitrary file read under the data root via the clip endpoints. The absolute-path branch checked containment against the whole data root rather than the clip directory, so an authenticated viewer could retrieve the accounts and detections databases. Both branches now contain to the channel or camera directory.
- Unauthenticated account creation with a client-supplied role, and an unauthenticated debug endpoint serving the broker URL with plaintext credentials and the site's coordinates. Both are now restricted to administrators.
- The guest account was not read-only — the role gate existed but was never applied, and a viewer could grant itself administrator on its own profile. Role is no longer settable on self-update, and the four state-changing endpoints require an administrator.
- The login page displayed working credentials. It now shows a rotation prompt naming the environment variables, and never a password; one-click guest sign-in moved to a server-side endpoint so no password ships to the browser.
- Sign-in brute-force protection is always on, rather than being gated behind the general API rate-limit switch that defaults off.
- Secrets are fully redacted in debug output rather than exposing a trailing fragment, credential-bearing URLs are redacted too, camera credentials no longer appear in capture logs, and session tokens no longer reach the access log on any launch path.
Removed¶
- Process machinery is no longer part of this repository — the backlog
flywheel's skills, review passes, and working notes. The work ledger
(
docs/backlog.json) holds outstanding work only; what shipped is recorded here and in What's new. - The legacy
orpheus-dashboardservice, superseded by the current UI, and a vendored third-party snapshot that nothing referenced.
Notes¶
- Age-based clip retention was implemented and then reverted before release.
Enforcing it would have deleted history on any station holding more than its
configured window implies — a station keeping a year of clips under a 30-day
setting loses eleven months on the first pass.
orpheus-storage-sweepanswers the same problem the other way round:floor_daysis a window that is never deleted rather than one after which everything is, so the sweep bounds the disk by size and can only ever remove recordings older than the floor.