Skip to content

room_view: mirror face_recognized into per-device state#103

Merged
BrettKinny merged 1 commit into
mainfrom
fix-room-view-state-mirror
May 23, 2026
Merged

room_view: mirror face_recognized into per-device state#103
BrettKinny merged 1 commit into
mainfrom
fix-room-view-state-mirror

Conversation

@BrettKinny
Copy link
Copy Markdown
Owner

Summary

Follow-up to PR #102. The room_view branch in `routes/vision.py` calls `state.broadcast()` on roster match but skipped the `state.update_state()` call that `POST /api/perception/event` normally pairs with broadcast. Result: `last_face_id` and `last_face_recognized_t` never landed on per-device state, even though the bus event fired correctly.

Why

Caught during the #44 LED bench sweep immediately after #102 merged. Brett walked in, named-greet fired correctly (event reached FaceGreeter), but the right-ring pixel 6 wouldn't hold green past the firmware's 4s timeout because `face_identified_refresher` reads `last_face_id` to decide whether to re-fire `set_face_identified`.

Live state confirmation pre-fix (snapshot from `curl /api/perception/state`):
```
"last_name_greet_t": {"brett": 1779532034} ← FaceGreeter saw the event

← no last_face_id field
```

Fix

Mirror what `POST /api/perception/event` does:
```python
state.update_state(device_id, "face_recognized", data, ts) # ← added
state.broadcast(PerceptionEvent(...))
```

Other affected readers that were silently no-op'ing:

  • `perception/snapshot.py` (`last_face_id` for talk-turn PerceptionSnapshot "Speaking with: …" hint to the LLM)
  • `state.get_fresh_face_id()` (canonical fresh-identity accessor)

Test plan

  • `pytest tests/test_routes_vision.py -q` — 10 passed
  • `pytest tests/ -q` — 198 passed (no regressions)
  • Existing happy-path integration test extended with assertions on `state.state[device]["last_face_id"] == "brett"` and `last_face_recognized_t > 0` — locks in the invariant so the regression can't reappear silently
  • Live walk-in: pixel 6 should now hold green while face_present=True (refresher fires every 3s while identity is fresh)

🤖 Generated with Claude Code

#102's broadcast emit fanned the event out to bus subscribers
(FaceGreeter / scene_synthesis) but skipped the per-device state
mutation that POST /api/perception/event normally does — so
`last_face_id` + `last_face_recognized_t` never landed on
state.state[device_id]. Downstream readers that depend on these
fields silently no-op'd:

- face_identified_refresher (reads last_face_id to keep right-ring
  pixel 6 green past the firmware's 4 s timeout — pixel was going
  yellow ↔ green ↔ off instead of holding green while the user
  stayed in frame)
- perception/snapshot.py reading last_face_id for the talk-turn
  PerceptionSnapshot ("Speaking with: …" hint to the LLM)
- state.get_fresh_face_id() — the canonical accessor

Caught during the #44 bench sweep right after #102 merged. The fix
mirrors what POST /api/perception/event does: update_state then
broadcast. The integration test grew assertions on the two state
fields so the regression can't reappear silently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 23, 2026 10:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a missing per-device perception state update in the room_view roster-match path of POST /api/vision/explain, ensuring last_face_id and last_face_recognized_t are written when a face_recognized bus event is emitted (matching the behavior of POST /api/perception/event). This unblocks downstream readers like face_identified_refresher, snapshots, and get_fresh_face_id() that depend on per-device state rather than just the broadcast event.

Changes:

  • Update routes/vision.py to call state.update_state(..., "face_recognized", ...) before broadcasting the face_recognized event.
  • Extend the existing room_view happy-path integration test to assert last_face_id and last_face_recognized_t are set.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
dotty-behaviour/routes/vision.py Mirrors /api/perception/event by updating per-device state before broadcasting face_recognized for room_view roster matches.
dotty-behaviour/tests/test_routes_vision.py Adds assertions to lock in the invariant that roster matches populate last_face_id / last_face_recognized_t.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +274 to +278
# Identity mirrored into per-device state (face_identified_
# refresher reads last_face_id to keep pixel 6 green past
# its 4 s firmware timeout). Bug caught during the #102
# bench sweep — broadcast alone wasn't enough; we also need
# to call update_state the way /api/perception/event does.
@BrettKinny BrettKinny merged commit 2b46d72 into main May 23, 2026
10 checks passed
@BrettKinny BrettKinny deleted the fix-room-view-state-mirror branch May 23, 2026 10:36
BrettKinny added a commit that referenced this pull request May 23, 2026
* #111: rip ACP/voice path from bridge.py

ACPClient + MessageIn/MessageOut + /api/message + /api/message/stream +
voice tool helpers + voice escalate/memory_log/remember/remember_person
+ _voice_preparer + _ConvoLogger are gone. /health drops acp_running /
cached_session / session_turns. The dashboard's per-person memory page
keeps its three brain.db read/mutate helpers
(_voice_memory_person_records_blocking / _voice_memory_approve_blocking
/ _voice_memory_delete_blocking) since /ui/memory is still live.

Perception consumers + vision/audio endpoints stay for commits 2-3 of
the #111 rip. _greeter_llm_client is stubbed to RuntimeError so the
ProactiveGreeter (still wired into lifespan until commit 2) fails fast
on its first prompt() call rather than NameError on a missing acp ref.

bridge.py: 6079 → 4564 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #111: rip perception consumers from bridge.py

All 13 in-process perception consumers — face_lost_aborter, sound_turner,
wake_word_turner, face_greeter, face_identified_refresher, purr_player,
named_acknowledger, scene_synthesis (loop is partial — see commit 3),
idle_photographer, sleep_dreamer, dance_reflector, security_capture,
ProactiveGreeter wiring — are gone. The in-memory perception bus
(_perception_state / _perception_listeners / _perception_recent_events
/ _sound_balance_history) goes with them, along with /api/perception/event
+ /api/perception/state + /api/perception/feed, the legacy convo-log
last_user_line cache, and the HouseholdRegistry / SpeakerResolver wiring.

Calendar machinery (_fetch_weather / _fetch_calendar_events / _refresh_caches
/ _calendar_poll_loop / summarize_for_prompt / _build_context /
_build_perception_block / Event TypedDict) goes too — dotty-behaviour
serves /api/calendar/today now (per #100 / dotty-behaviour/routes/calendar.py).

The dashboard perception card now reads empty stubs
(_dashboard_perception_state_getter / *_recent_getter /
*_last_user_line_getter / *_sound_balance_series / *_vision_failures_last_hour
all return empty), so the card renders without errors but shows no data
until the dashboard ports to dotty-behaviour. _identity_display_name
returns None for the same reason.

Dispatch helpers kept: _dispatch_abort, _dispatch_set_state,
_dispatch_set_tier1slim_model, _dispatch_set_toggle — all four are
needed by the dashboard / admin endpoints. The consumer-only dispatchers
(face_greeting / say / set_head_angles / set_face_identified /
purr_audio) went with the consumers.

bridge.py: 4564 → 2868 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* #111: rip VLM / vision / audio code from bridge.py

/api/vision/explain, /api/vision/latest/{device_id}, /api/audio/explain
are gone. The room_view sentinel branch (VISION_ROOM_VIEW_SYSTEM_PROMPT,
__ROOM_VIEW_V1__, _build_room_view_question, _parse_room_view_response,
_ROOM_VIEW_* constants) goes with it — dotty-behaviour now owns the
room_view roster recognition path (PR #101 / #102 / #103).

_call_vision_api + VLM HTTP plumbing + _classify_vision_result +
_call_narrative_llm + _call_audio_caption_api + _audio_format_from_upload
go too — dotty-behaviour owns every VLM call now (and dotty-pi-ext owns
narrative calls from voice tools).

Scene-synthesis / idle-photographer / dream / dance NDJSON writers
(_scene_synthesis_log_path / _idle_perception_log_path /
_write_idle_perception_record / _is_notable_perception /
_idle_photographer_pick_device / _dreams_log_path / _dances_log_path /
_write_jsonl_record / _write_dream_record / _write_dance_record /
_split_dream_text / _compose_scene_synthesis /
_write_scene_synthesis_ndjson / _maybe_emit_scene_synthesis /
_perception_sleep_dreamer / _perception_dance_reflector /
_perception_idle_photographer / _scene_synthesis_loop) are gone — every
consumer they fed lives in dotty-behaviour.

The legacy /admin/* endpoints that targeted the retired ZeroClaw daemon
(_apply_model_swap, _read_voice_model_from_cfg,
_voice_profile_for_model, _admin_schedule_restart, _ADMIN_DAEMON_CFG,
/admin/model) were retired alongside — /admin/persona stays for
external persona-file edits, /admin/safety stays as a static
MCP_TOOL_ALLOWLIST mutation surface, /admin/smart-mode keeps just the
tier1slim hot-swap path, /admin/state + /admin/kid-mode keep their
xiaozhi-admin passthrough.

bridge.py: 2868 → 911 lines.

Final state: dashboard-only. Cache stubs (_vision_cache / _audio_cache /
_scene_synthesis_cache / _perception_state) all empty dicts so the
dashboard perception card renders without errors but shows no data
until it ports to dotty-behaviour. The brain.db memory funcs
(_voice_memory_person_records_blocking / *_approve_blocking /
*_delete_blocking) survive because /ui/memory is the only operator
surface on per-person memory rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants