Skip to content

wakeword: treat empty config as disabled - #101

Merged
ai-hpc merged 1 commit into
GeniePod:mainfrom
Crystora:fix/issue-100-wakeword-disabled-state
May 18, 2026
Merged

wakeword: treat empty config as disabled#101
ai-hpc merged 1 commit into
GeniePod:mainfrom
Crystora:fix/issue-100-wakeword-disabled-state

Conversation

@Crystora

Copy link
Copy Markdown
Contributor

Summary

  • treat empty [core].wakeword_script as explicit push-to-talk mode in start_all.sh
  • stop and reset-failed genie-wakeword.service when wakeword is disabled by config
  • report wakeword as Disabled / config in /api/services instead of surfacing stale failed systemd state

Fixes #100

Testing

  • cargo fmt --check
  • cargo test -p genie-api
  • cargo test -p genie-core --test tool_dispatch_test -- jetson_lifecycle_scripts_are_valid_shell start_all_uses_configured_llm_backend
  • bash -n deploy/scripts/start_all.sh
  • local fake-systemctl simulation for wakeword_script = ""
  • Jetson deploy + dashboard/API validation

Real Behavior Proof

  • I have built and run the affected code locally.
  • I have verified the change end-to-end on Jetson hardware.

Jetson dashboard result after deploying this branch:

core          Healthy       87ms
llm           Healthy       5ms
api           Healthy       0ms live
health        Healthy       n/a
governor      Healthy       n/a
mqtt          auto-restart  n/a
audio         Healthy       n/a
whisper       Healthy       n/a
wakeword      Disabled      n/a
homeassistant Healthy       n/a

@ai-hpc ai-hpc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closes #100 cleanly with a three-layer fix at exactly the right boundaries:

  1. deploy/scripts/start_all.sh doesn't try to start the unit when wakeword is disabled — and proactively systemctl stop + reset-failed it to clear any stale failed state from a previous run. That alone fixes the dashboard symptom going forward, but doesn't help boxes that already have the failed state cached, hence the next two layers.
  2. /api/services short-circuits the merge when disabled_reason is set — returns a synthetic row with healthy: true, source: "config", sub_state: "disabled", and the disabled-reason as the error string. Crucially, the wakeword target is also dropped from unique_units() so the API doesn't even fork systemctl show for it. Saves a probe and ensures the merge result genuinely doesn't depend on whatever stale systemd state the box is sitting on.
  3. dashboard.js serviceStatus() gets a if (s.source === 'config') return { label: 'Disabled', ... } first-branch that renders gray rather than green/red, so the visual mapping matches the data model.

Each layer is necessary, none is redundant. Drop the systemd-stop and a box that previously failed stays red until reboot. Drop the API short-circuit and the dashboard still has to interpret a failed row as Disabled from JS alone (fragile across future renders). Drop the UI branch and source: "config" shows up as "Unknown".

disabled_reason: Option<String> on ServiceTarget is a clean extension point — other services could plumb the same shape later (e.g., homeassistant when no ha_token, mqtt when broker isn't configured) without re-architecting.

The trickiest piece is the __missing__ sentinel in read_wakeword_script:

END { if (!found) print "__missing__" }

This distinguishes "key absent from the toml entirely" (legacy config, use the compiled default) from "key present but empty value" (explicit operator opt-out). Without that, a geniepod.toml from a pre-#100 deploy would suddenly start treating wakeword as disabled on the next start_all.sh, which would be a silent regression for anyone running on default config. The sentinel + fallback line preserves the existing semantics exactly. Right call.

Test coverage is the right shape:

  • dashboard_targets_mark_wakeword_disabled_when_config_empty — pins that empty config → disabled_reason: Some(...) AND that the unit gets dropped from the probe list. Catches a regression where someone restores the unique_units line and starts re-probing the disabled unit.
  • disabled_wakeword_rows_ignore_stale_failed_systemd_state is the killer test: even with active_state: "failed" in the injected SystemdRow, the disabled flag wins and the merged row reports healthy: true with source: "config". This is the exact scenario from #100 (box has a cached failed state) and the test pins that the disabled path supersedes it.
  • start_all_uses_configured_llm_backend extended with three new assertions: the read_wakeword_script function exists, the push-to-talk mode skip reason string is present, and reset-failed "$unit" is called for disabled services. Pins the start_all-side behavior so a future shell refactor can't quietly undo the reset-failed step.

The operator's Jetson dashboard snapshot in the PR body is the end-to-end verification — wakeword now renders as Disabled with n/a latency, the row no longer looks broken, and the rest of the stack reads exactly the same as before.

Two minor things to flag, neither blocking:

  • The disabled_reason string is hardcoded at dashboard_service_targets rather than being i18n-able or pulled from config. That's fine for an alpha — single English deployment surface — and the symmetry with the existing error-string handling means contributors don't have to learn a new pattern. Worth noting in case a future PR wants to translate dashboard strings.
  • unique_units filter drops disabled targets from the probe list, but systemd BTreeMap entries for those units could still appear if some other code path inserts them (e.g., a future probe that walks systemd state independently). Not a current issue — the only producer of the map is query_systemd_unit invoked from the loop that consumes unique_units — but a merge_service_rows test asserting that a stale systemd entry for a disabled unit is ignored would close the door on that class of regression. Out of scope for this PR.

All 8 CI checks green on d9d8835 (fmt, clippy, test, aarch64 cross-compile, --no-default-features, shellcheck, ruff, PR body checklist). LGTM.

@ai-hpc
ai-hpc merged commit 1d94369 into GeniePod:main May 18, 2026
8 checks passed
@ai-hpc

ai-hpc commented May 18, 2026

Copy link
Copy Markdown
Contributor

Merged at 1d943694b6366e5c9a9b3dc6acb6e95131f98ad7.
Thanks to @bittoby!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wakeword service fails while rest of stack is healthy

2 participants