Eufy Native Current-Room Transition Detection (Hybrid)¶
Status: design proposed 2026-06-19; Wave 0 validation CAPTURED 2026-06-20. The freshness gate PASSED (signal is live server-side with the map tab fully closed), and a blind room-attribution test recovered the exact cleaned set (3/3, precision = recall = 1.0) on a deliberately adversarial external run β dock-trap avoided, tiny-room floor caught. Direction approved (hybrid: native-primary + heuristic fallback). All three adversarial cases PASSED β 9/9 cleaned-room calls, 0 false positives across: dock-trap (room 8 excluded when parked), dock-room-cleaned (room 8 included when cleaned, parkedβcleaned flip on spread), tiny-room floor (1 mΒ² / 30 s), and the interleaved mop-wash "vicious" run (3-way cross-check: probe β record β device history; cleaned-area confirmed as the wash/clean tiebreaker). Still APPROVAL PAUSE on runtime code: the meanest realistic cases held, so what remains before code is to formalize the dwell + spread + winding + cleaned-area rule with the validated thresholds (and re-confirm the file:line anchors). See Wave 0 results. Anchors below are as-investigated and must be re-confirmed at implementation time (verify-vs-code rule).
The question¶
Eufy now exposes a per-frame current_room (read from eufy-clean's in-memory MapData,
surfaced on the map-overlays sensor). Historically Eufy had no native current-room, so run-time
room-transition detection used the counter-plateau / timing heuristic (eufy_counter_v1).
Roborock instead gets a native rollover via the brand-agnostic live_transition seam. Can
Eufy's current_room now drive transition detection through that same seam?
Verdict¶
Yes β with caveats β via the EXISTING live_transition.native_transition_source seam. The seam
has zero brand-specific engine code, so an Eufy adapter could in principle ride the same
_maybe_roll_current_room_by_native_signal path Roborock uses. But the signal is the wrong shape
today, and it is itself an inference β so the answer is a hybrid, not a flag flip.
Two facts shape the whole design:
- Shape mismatch. The seam consumes a live room-NAME entity (slug-matched to job targets,
_resolve_native_target_room_idatjobs/active_job.py:1048) wired asentities.active_cleaning_target. Eufy'scurrent_roomis an inferred raster-lookup room ID (mapping/map_source.py:231-260, surfaced atsensor/map_overlays.py:72-84), and Eufy already usesactive_cleaning_targetas a completion sentinel (adapters/eufy/adapter.py:353). So a name-surfacing + slug-reconciliation shim and a completion-path migration are prerequisites. - It's inference, not an upstream signal. Our "prefer dedicated upstream signals over inferred
state" principle would push native-only β but Eufy's
current_roomis a raster lookup we compute, not a device field. It is a better-grounded inference than counter-plateau, yet it goes blind (None) exactly when the robot is between rooms or docked. That is precisely where the heuristic must remain as a fallback.
The seam (existing, brand-agnostic)¶
Three collaborators (jobs/active_job.py, _maybe_roll_current_room_by_timing at :814):
live_transition.native_transition_source: Trueβ Eufy is currentlyFalse(adapters/eufy/adapter.py:632).entities.active_cleaning_targetβ a live room-NAME sensor.- the ~5s tick caller
_maybe_roll_current_room_by_timingβ a**kwargsdelegator on the manager (core/manager.py:843) called from the tick atcore/manager.py:3139, whose real implementation isjobs/active_job.py:814β short-circuits into the native branch_maybe_roll_current_room_by_native_signal(jobs/active_job.py:1096) when the flag is set.
Native rollover logic is order-agnostic and idempotent
(_maybe_roll_current_room_by_native_signal, jobs/active_job.py:1096):
first-confirmed target is adopted with no completion (queue order was a guess); a move to a
different target completes only the previously-confirmed target; same-target is a no-op. Unknown /
transit / non-job-target names resolve to None and are ignored (in
_resolve_native_target_room_id, jobs/active_job.py:1048) β this is the built-in
transit + dock filter. Roborock proves coordinate drift is irrelevant here: its rollover is
purely name-driven and uses no position/bounds (position_lock_reliable is also False).
Sequenced/strict-order jobs bypass both paths (the if active_job.get("phases") guard,
jobs/active_job.py:852) and instead read the same native signal through the strict-order phase
watchdog. That watchdog now lives in jobs/phase_runner.py (class PhaseRunner), not
core/manager.py (it moved out in the feat/zone-clean re-bundle); it calls the
native_current_room_target_id accessor β a method of ActiveJobTracker
(jobs/active_job.py:1039) β via self._manager.active_job.native_current_room_target_id(...) at
jobs/phase_runner.py:450.
Dock-start phantom is already handled (not a new edge case). When the dock sits inside a queued
room, current_room reads that room while parked, then changes as the robot leaves β naively
"completing" a room that was never cleaned. This is documented + tested: NR-10 (sequenced jobs no-op
the live rollover via the phases guard) and NR-4 (the first confirmed signal is adopted
with no completion). Eufy inherits both by riding the seam; we just mirror those cases in the Eufy
contract tests (shim #6). Confirmed empirically in Wave 0 β the dock room was seen but correctly not
attributed (see below).
The hybrid design¶
Native-primary, heuristic-fallback, gated on availability + an N-frame settle.
- When
current_roomresolves to a confirmed job-target name and has held for N consecutive ticks, the native path completes/advances. - When it is
None/ stale / off-map / docked, fall back to the dormanteufy_counter_v1counter-plateau engine (kept wired atadapters/eufy/adapter.py:613,counter_segmentation.py). - The settle requirement replaces the smoothing the plateau logic gave for free β the native
rollover path has no debounce of its own (settle/verify/retry lives only in the strict-order
phase watchdog
PhaseRunner._run_advanced_phase,jobs/phase_runner.py:287, with the re-dispatch at_dispatch_active_phase,jobs/phase_runner.py:486β not in grouped-job rollover). The watchdog moved out ofcore/manager.pytojobs/phase_runner.pyin thefeat/zone-cleanre-bundle;core/manager.pykeeps only amaybe_advance_phasedelegator (core/manager.py:4206) and spawns the initial phase viaself.phase_runner._run_advanced_phase(core/manager.py:4403). The_PHASE_*timing constants (core/manager.py:85-110) and the_phase_timingresolver (core/manager.py:4220) stayed on the manager.
This captures most of the better-grounded-signal win while the heuristic covers the inference's blind spots.
Prerequisite shims¶
| # | Shim | Why | Anchor |
|---|---|---|---|
| 1 | Live current-room-NAME sensor (rid β room.number β managed name, slug-reconciled) |
seam matches by slug, not id | mapping/map_source.py:201,:259, sensor/map_overlays.py:82 |
| 2 | Migrate Eufy completion off the active_cleaning_target sentinel β adopt require_job_active_clear: True |
frees the entity to carry the live name (Roborock's approach) | adapters/eufy/adapter.py:353, adapters/roborock/adapter.py:201 |
| 3 | Server-side refresh of the rid/name during a run, independent of the map tab | the 2s freshness is UI-poll-coupled (src/cards/main.js:600-625) and does NOT run when the tab is backgrounded |
new periodic task |
| 4 | N-frame settle on the native rollover for non-phased jobs | no built-in debounce; doorway cells can flicker the rid for one frame | _maybe_roll_current_room_by_native_signal, jobs/active_job.py:1096 |
| 5 | Re-map reconciliation: re-resolve ridβname when the raster version changes | rid raster is content-versioned (eufy_version_of, sha1) |
mapping/map_source.py:449-456, cf. adapters/roborock/adapter.py:274 |
| 6 | Contract tests mirroring NR-1..NR-11 for the Eufy adapter |
parity with Roborock's native-rollover suite | tests/integration/test_native_rollover.py |
The flag flip + per-room-live-settings reuse are nearly free; shims 1β4 are the real work.
Wave 0 β the validation gate (CAPTURED 2026-06-20)¶
Instrumentation: throwaway server-side service eufy_vacuum.debug_log_live_room (in
mapping/mapping_services.py, REMOVE after validation) β resolves the live pose every 2s via the same
async_get_map_live_pose path and logs {t, current_room, robot_docked, robot_anchor, β¦} to
config/eufy_current_room_probe_<vacuum>.jsonl, with no card open. Two runs captured on
vacuum.alfred: a 2-room dispatched run + a deliberately adversarial 3-room external run.
Result β the gate PASSED on all three numbers:
1. Refresh server-side, card closed: YES. 351 distinct anchors / 375 consecutive changes over the
external run β eufy-clean pushes pose via MQTT independent of any UI poll. So shim #3 may be
unnecessary (the signal is already live without us driving it). Caveat: no in-repo proof the
_robot_pixel frame is stable mid-run; observed-only.
2. Flicker: 0 single-tick room blips on the dispatched run; on the external run the only 1-tick
blips were None/transit cells and post-dock previous-room flashes, all filtered by dwell.
3. None-while-cleaning: 0%. The signal never went blind through either run, even across a 43 s
inter-room transit.
Decisive headline β blind room attribution recovered the exact cleaned set. From current_room
alone (no labels), classifying each room the signal saw by dwell + anchor spread + path-winding,
the predicted cleaned set {2, 4, 6} matched the ground-truth external job record exactly:
| Room | Name / area | Signal | Verdict |
|---|---|---|---|
| 4 | Hallway 6 mΒ² | dwell 118 s, spread 0.069, winding 126 | CLEANED β |
| 2 | Bathroom 2 mΒ² | dwell 110 s, spread 0.054, winding 31 | CLEANED β |
| 6 | Entryway 1 mΒ² | dwell 46 s, spread 0.026, winding 5.2 | CLEANED β (tiny floor) |
| 8 | Dining (dock) | dwell 271 s (longest!), spread 0.015 | dock β correctly NOT attributed |
| 7 | β | 8Γ ~16 s, winding ~1.2 | transit hallway β not attributed |
The two hard cases both broke right: the dock trap (room 8 had the longest dwell of the whole run and would be the #1 false positive on dwell alone β only its low spread excluded it) and the tiny-room floor (entryway, 1 mΒ², ~60 s clean β cleared the transit band: min-cleaned 46 s vs max-transit 18 s, a 28 s gap).
Caveats (why the gate isn't fully closed):
- n=1 per pattern (2 runs). Thresholds were interpreted on this data, not pre-registered.
- The naΓ―ve auto-classifier mis-included the dock (room 8) on dwell; the spread-rescue rule is
what excluded it. So the signal contains enough to be exact, but the rule (dwell + spread + winding,
with a dock spread-rescue) must be formalized and re-validated before it runs unsupervised.
- ~~Dock-room-cleaned is untested.~~ CLOSED 2026-06-20 (run #2, dock-room-first). Dining
(room 8, the dock room) was cleaned first; the classifier flipped it parkedβcleaned correctly on
spread alone: cleaned-dock spread 0.073 (one contiguous park+clean run, cleaning dominates) vs
parked-dock ~0.015β0.016 (last run + this run's end park) β a ~4.5Γ gap, same physical room,
both directions correct. Blind prediction {2, 6, 8} = ground truth exactly (precision = recall = 1.0).
The max-spread-per-run aggregation was validated (room 8 had both a cleaned run and a park run;
max picked the clean). Combined 6/6 cleaned-room calls across the two adversarial external runs,
0 false positives. (then 6/6.)
- Interleaved mop-wash (the "vicious" case): PASSED 2026-06-20 (run #3) β now 9/9 across all three
adversarial runs, 0 false positives. Kitchen(mop)βDining(vacuum, the dock room)βHallway(vacuum+mop)
with mid-run washes; blind {4,5,8} = ground truth. Room 8 had 5 runs; max-spread-per-run
picked the dining clean (spread 0.071) out of the start-wash/early-clean/clean+folded-wash/park/
final-wash pile. A three-way cross-check β probe current_room β finalized record β device-history
CSV β all agreed. Cleaned-area (sensor.*_cleaning_area) validated as the wash/clean tiebreaker:
swept mΒ² accrues during cleans (kitchen ~2, dining ~8, hallway ~2) and is FLAT during the wash
(03:44β03:46, vacuum=docked/dock=Washing, 0 mΒ² added) β separating "washing in the dock room" from
"cleaning the dock room" when spread can't (same room id, contiguous). Measured (not assumed): the
device does NOT wash on every mode switch (no wash between kitchen-mop and dining-vacuum) β real
cadence β predicted. And sensor.*_active_cleaning_target stayed None the whole external run β
the in-job seam's name-signal is unavailable for app cleans, so external runs must use this
classifier (β W5), not the seam. Set attribution is robust on dwell+spread+winding; time
attribution needs cleaned-area to subtract the folded wash (run 5 = ~600s clean + ~170s wash).
External-run auto-attribution (first-class consumer β validated)¶
The headline use is not just the in-job rollover. An app-started (external) clean has no
dispatched queue, so the integration can't anchor to targets β today it leans on the counter-plateau
heuristic + a manual room-set step in the external-capture wizard. The Wave 0 external run was exactly
this case, and current_room recovered the cleaned rooms exactly (3/3) β so it can auto-attribute
external runs instead of inferring/asking. Treated here as a first-class consumer, built to the same
bar (rare-use is no reason to half-build it; and it rides the same signal as the in-job rollover, so
it's cheap to add).
Two concrete wins seen in the data:
- The external job record logged transitions: [] β the capture system recorded zero
transitions β while current_room captured the full device path (8β7β4ββ¦β2ββ¦β6β8) with clean
handoffs. So the native signal adds path/transition data the capture system doesn't have today.
- The dwell+spread+winding classifier separated the 3 cleaned rooms from the dock + transit rooms with
clean margins (see the Wave 0 table).
The classification rule β FORMALIZED 2026-06-20 (scratch-external-estimator/room_attribution.py
+ test_room_attribution.py, a pure-Python prototype with the 3 runs as regression fixtures):
1. Segment by current_room into contiguous runs; per run compute dwell, anchor spread (RMS),
path-winding (path_len / net_disp), bbox area.
2. Drop TRANSIT by winding β a run with winding < ~1.5 is a straight pass-through (transit
rooms measured 1.0β1.22; cleaned rooms β₯ 4.9). Robust across all 3 runs.
3. Cleaned vs parked-dock by SWEPT AREA β sensor.<vac>_cleaning_area delta over the room's
windows β₯ ~0.5 mΒ² β cleaned; a wash/park sweeps ~0 mΒ². Aggregate per room by its best run
(NOT total dwell).
KEY FINDING the formalization surfaced β swept-area is REQUIRED, not optional. The anchor-only
signals (dwell + spread + winding) cannot separate a jittering parked dock from a clean: in
run #1, room 8 (parked) sits inside the cleaned cluster on every anchor axis (dwell 271 s, winding 43,
spread 0.028 β all β₯ the cleaned tiny rooms). The harness proves it β anchor-only = 9/9 recall but
1 false positive (the parked dock leaks through); the live "9/9" had silently patched that with
manual judgment on that exact room. Area-augmented = 9/9, 0 FP. So the earlier "spread-rescue"
framing was not robust; the device swept-area is the load-bearing clean/parked separator. (The probe
debug_log_live_room now logs cleaning_area + task_status/dock_status so future runs carry it
natively.) Distinct from the in-job seam (job-target slug match, unavailable for external runs), this
is a segment-by-current_room + swept-area classifier β the role the counter-plateau heuristic
plays today, but grounded in observed position + device area.
Waves (post-gate, pending approval)¶
- W0.5 β DONE: all adversarial runs captured + the rule formalized with regression fixtures
(
scratch-external-estimator/). Outstanding: the probe now logscleaning_area, so capture a couple more runs to re-validate the area-augmented rule end-to-end (the anchor-only fixtures used the record's per-room area as a stand-in for runs #1/#2; run #3 was device-verified). - W1 β shim #1 (name sensor) + shim #2 (completion migration), with tests. No behavior change yet.
- W2 β shim #3 (server-side run-time refresh) only if W0.5 contradicts the "already-live" finding.
- W3 β flip
native_transition_source+ shim #4 (settle) + availability fallback gate;NR-parity tests. - W4 β strict-order path via
native_current_room_target_id+ shim #5 (re-map reconciliation). - W5 β external-run auto-attribution (chosen first; planned by the w5-external-attribution-plan workflow). Wires the classifier into the external-capture path to auto-derive cleaned rooms, gated on a confidence check with the manual wizard as fallback. Sliced:
- W5a β DONE 2026-06-20. Ported the classifier to a pluggable engine
learning/room_attribution_engines.py(eufy_anchor_winding_v1, mirrors thejob_segmenterseam: Protocol + by-refDEFAULT_TUNING+ Eufy-fallback registry); added the swept-area-from-cleaning_area-timeline derivation the prototype received pre-aligned; declared the adapterroom_attributionblock (adapters/eufy/adapter.py) + registry validation. Ships DORMANT β no consumer yet. Tests:tests/unit/test_room_attribution_engines.py(RA-1..10, seam) +tests/adapters/eufy/test_room_attribution.py(the 3 adversarial runs, 9/9 area + the anchor-only dock false-positive + a synthetic full-pipeline). Full suite green (2527). - W5b β DONE 2026-06-20 (built + adversarially reviewed). Run-active sampler
listeners/pose_sampler.py(cadence + gating from the adapter'sroom_attributionblock; EXTERNAL-only;map_state_source-gated) appends{current_room, anchor, cleaning_area}topose_samplesviarecord_pose_sample(jobs/active_job.py). Capture-only/inert. The review caught + fixed: a real robust-mode false-negative (swept-area is now authoritative over the winding drop), docked-tick buffer poisoning (sampler nullscurrent_room/anchoronrobot_dockedβ a parked dock is a genuine None-run), and an adapter-discipline miss (cleaning_areanow read fromentities.cleaning_area, not a guessed name). Tests:tests/unit/test_pose_sampler.py+record_pose_samplecases intest_jobs_active_job.py; full suite green (2543). - W5b live experiment β DONE 2026-06-20, PASS. One real 5-room external clean on
vacuum.alfred(rooms 5,6,7,8,9; 404 sampler ticks / 13.5 min), sampler + throwaway probe both capturing. Diff (scratch-external-estimator/w5b_diff.py, time-aligned on overlap): freshness nearest-probe |dt| median 1.0s = pure 2s-cadence phase, no staleness; frame anchor coord ranges identical and coincident-tick (|dt|β€0.6s, n=120) anchor distance median 0.000 β the sampler reads the same live value as the probe, so anchor-unit thresholds port directly; transitions macro room-sequence matches the probe, the 7 disagreements (98.3% agree) are all 1-tick flickers / ~1s boundary phase present on BOTH sides (no systematic lag). Preserved native fixture:scratch-external-estimator/w5b_live_pose_samples.json. Notable: this run's counter-plateau finalize wrote NO record (build_pending_record β None, 25 counter samples, no plateaus) and the slot was clearedexternalβidleon normal completion βpose_samplesmust be read out of.storagebefore that reset (they're dropped from the record AND wiped). Recorder ground truth: cleaning 08:13:11 β returning 08:23:44 β docked/Completed08:24:40 (a clean finalize, NOT premature β an early probe-flag-based "premature finalize" read was wrong; the probe'srobot_dockedstayedFalsethrough the real dock). - W5c previewed on the real samples (
scratch-external-estimator/w5b_attribute_real.py, the engine run directly on the 404 native ticks):mode=robust,cleaned=[5 (kitchen, 6 mΒ²), 9 (~2 mΒ²)]. The win: room 8 (the dock room) had the MOST presence of any room β 100 ticks / 200 s β but 0.0 mΒ² swept βparked/dock, not cleaned. Hard version of the trap: the pose source reported the parked robot as "moving in room 8" the whole time (robot_dockednever flipped) ANDcleaning_areawas non-monotonic (stale 16 β reset β 10); the positive-delta swept logic absorbed both. Counter-segmentation found nothing here β argues W5c should let the pose path stand up a record on its own, not only enrich a counter-segmented one. (cleaned set still wants the user's ground truth of what the app actually cleaned.) - Refinement W5c must carry β docked-gate signal. The sampler's F2 docked-nulling keys off the
pose source's
robot_docked, which is UNRELIABLE (stayedFalsethrough the 08:24:40 dock β the null path never fired; 100 dock/return ticks were recorded as room 8 and only swept-area excluded them). Gate docked-nulling on the MQTT-backedtask_status/vacuum state instead (Returning/Completed/docked/Chargingβ these flipped cleanly and on-time), so the parked-dock exclusion has an independent reliable signal and doesn't lean solely on swept-area (which itself can be flaky, ascleaning_areawas here). - W5c β DONE 2026-06-20 (built + adversarially reviewed). Pose attribution wired into the
external-run finalize, backend-only (the card already auto-selects
shortlist[0], so promoting the classified room there pre-answers the wizard with NO frontend change):learning/external_ingest.py:_resolve_attribution/_attribute(engine + tuning from the adapter'sroom_attributionblock, Eufy fallback β mirrors_resolve_engine_tuning);_apply_pose_identity+_dominant_room+_promote_pose_roomENRICH each counter segment with its dominant cleaned room βshortlist[0](ROBUST mode only β anchor-only can false-positive a parked dock, so it doesn't override the settings shortlist);build_attributed_jobSTANDS UP a pose-only record when the counter segmenter finds nothing (the common app-run case β this morning's run produced no counter record).build_pending_recordgained apose_samplesparam;attribution_modeis stamped on the record for the card.listeners/pose_sampler.py:_is_parkedβ the docked-gate now reads the MQTTtask_statusvs the adapter'svocabulary.active_run_task_states(reliable), falling back to the poserobot_dockedflag only when task_status can't be read (the F2-via-MQTT refinement above).core/manager.py:_finalize_external_runpassesslot["pose_samples"]through and now ALWAYS clears the slot (try/finally) so a build error can't orphan astatus="external"zombie.- Hybrid gate: no pose stream / empty cleaned set β
attributionis None β exactly pre-W5c behavior (availability fallback). robust vs anchor_only ridesattribution_mode. - Adversarial review (16 agents): 5 findings survived refutation; only one was a real fix β
an uncaught
engine.attribute()exception that dropped the run AND orphaned the slot. FIXED:_attributedegrades to counter-only on engine error + the finalize try/finally always clears. The other 4 were verified-but-inert (deadgap_transit_sfield, missingsourcelabel on enrich records, pose metadata lost on re-segment, unreachable out-of-orderwall_s) β documented, no change. Tests:tests/unit/test_external_ingest_attribution.py,test_pose_sampler.py(MQTT gate incl. the exact live failure),tests/integration/test_manager_external_finalize.py(EXT-FIN-2 pose-only finalize, EXT-FIN-3 slot-clears-on-error). Full suite 2559 pass / 1 skip.
- W5d (later) β opt-in auto-confirm for proven high-confidence robust runs.
W5 gating + adapter discipline. The native path rides current_room, which is derived from
map data (current_room_for_pixel over eufy-clean's in-memory MapData raster β map_source.py:231;
no map β async_get_map_live_pose returns {present:false, reason:"no_geom"}. That method moved to
mapping/map_source_coordinator.py (class MapSourceCoordinator); the no_geom return is at
mapping/map_source_coordinator.py:412. core/manager.py:3742 keeps only a thin delegator that calls
self.map_source.async_get_map_live_pose β the manager's MapSourceCoordinator instance is held on
the self.map_source attribute, constructed at core/manager.py:398).
So: no map β current_room is None β the engine returns an empty cleaned set β W5c's
availability gate falls back to today's manual wizard (and the in-job track falls back to the
map-independent counter-plateau heuristic). W5 is therefore purely additive β with the live map
you get the native signal, without it you get exactly today's behavior. Two consequences for the
build:
- W5b's sampler gates on map_state_source presence β don't sample pose for a vacuum with no
live map (the rows would be all-None).
- All brand settings stay in the adapter. The engine choice, the thresholds
(wind_transit/dwell_min_ticks/swept_area_min_m2), and the sampler interval_s all come
from the adapter's room_attribution block β the single
operative source, mirroring job_segmenter.tuning. Core listeners/ + external_ingest read
those (resolved via a helper like _resolve_engine_tuning); they must never hardcode the
cadence or thresholds. Same rule for the W5c confidence/availability gate values.
Shipped (1.8.0) β both brands, external + dispatched¶
The native-attribution path is live for Eufy and Roborock, on app-started (external) and dispatched runs, validated across simultaneous multi-room cleans on both robots (2026-07-11).
- Capture source is adapter-declared (
room_attribution.source): Eufy reads the fork's decoded-map pixel pose (live_pose); Roborock reads its native current-room NAME entity (native_current_room), which the sampler slugifies and matches to a managed room id. The pose sampler buffers both external and dispatched runs. - External runs flow through the existing counter/pose consumption (
build_pending_recordβbuild_attributed_job/_apply_pose_identity) β a pre-answered review record. Roborock (noop segmenter) always takes the pose-only path; the clean decision is the engine's pose-free swept-area robust mode, so it needs no pixel anchors. - Dispatched runs β the atomic finalize's positional (segment K β queue room K) identity is
reconciled against the native current-room (
external_ingest.reconcile_dispatched_identity): confirm on agreement, rescue when the positional map is already unreliable, flag (attribution_disagreement, surfaced as the card "Room Mismatch" badge) on a confident disagreement β never silently overriding. Strict-order (phased) jobs already capture per-phase timings and are left untouched. cleaning_areais normalized to canonical mΒ² by each sensor'sunit_of_measurement(learning/utils.cleaning_area_to_m2), live-read at every capture β an imperial HA presents Eufy's sensor in ftΒ² and Roborock's in mΒ². Swept-area sums positive per-tick increments so a non-monotonic (reset) counter is re-baselined, not double-counted. The device's own run total (cleaning_area_sensor_m2) is the sanity bound:attributed_sum > sensor_totalβarea_over_attributed.- Card surfacing: an Origin filter (external / dispatched), the Room Mismatch badge, and Area Cleaned on external runs.
Open unknowns (honest)¶
- No in-code assertion that the live
_robot_pixelframe is stable mid-run β inferred from eufy-clean's behavior, not proven here (mapping/map_source.py:407-415). - Deviceβeufy-cleanβrender latency rides on top of our freshness guarantee and is outside this repo; the seam's idempotency absorbs a coarse cadence, but the settle value (N) must come from W0 data.
Related¶
docs/dev/map-state-source.mdβ where the rid signal comes from.docs/dev/29-roborock-adapter.mdβ the native-rollover precedent.docs/dev/06-job-lifecycle.mdβ the rollover tick + phase model.- memory:
reference_eufy_intersession_coord_drift,project_room_segmentation_unified,feedback_kiss_upstream_signals,feedback_archive_cheap_raw_data,feedback_quality_not_gated_by_usage(why external-run auto-attribution is first-class despite rare use).