Conversation
The diagnostics dump described an appliance purely through its shadow: the telemetry it happens to publish. That cannot answer what the appliance IS. A fridge that reports `tempZ3` may simply not have that zone, and the dump gave no way to tell the two apart, so a zone-indexing report needed a round trip to the reporter before it could even be diagnosed (issue #75). The cloud already sends the answer in `applianceModel.attributes`: `zones`, `seriesVersion`, `vtRoom1`/`vtRoom2`, `doorNumber`. We were fetching it and keeping only `options`. The hOn app itself treats those rows as authoritative where the shadow is not - it decides which fridge zones exist from `zones`.split("|"), never from which `tempZ*` keys the shadow carries. Expose them as `HonAppliance.model_attributes`, flattened parName -> parValue the same way __init__ already flattens the appliance-level attributes, and emit them as a `model_attributes` block ahead of `attributes`: what the model is, then what it is doing. Read off the appliance rather than the coordinator entry, since it is per-model and immutable for the session. Redaction is unchanged - the block goes through _redact like every other section.
A field report has been open for two weeks: an HHP50CA011 shows every purifier
control except Child Lock and Sounds. Neither artifact we ask for could answer
it, and that is a hole in this integration, not bad luck.
The AP branch of the switch platform gated on two conditions and did a bare
`continue` on each, while the summary line named only what it DID build. So a
purifier missing a toggle looked exactly like a purifier that never reached the
branch. The sibling platforms have always logged their skips: light.py names the
capability, the live values and the reported state, and select.py does the same
for the aroma and panel-light selects. The AP switches now match them, and the
line separates the two gates, so "the schema does not declare it" and "the device
does not report it" stop looking alike.
The dump had the matching hole. `_param_schema` emits min/max/step for a range,
but param_range() casts through float(), so a schema spelling its bounds "0"/"1"
and one spelling them "0.0"/"1.0" print identically -- while the materialised
grid differs, and the capability gates compare exactly those strings
(`lock_values == {"0", "1"}`). A single decimal-spelled bound removes a control
and left no trace anywhere. Small grids now carry their values.
The bound is what keeps the standing rule intact: a setpoint range is still never
enumerated, because the point count is computed arithmetically from min/max/step
and `.values` is never read for a grid over the cap. A test proves that by making
`.values` raise.
Deliberately NOT changed: the gates themselves. They compare strings, which is
the trap described above, but for this reporter they are not the cause -- the
same funnel feeds the fan, both selects and both timing numbers, and all of those
exist on his unit, so his schema is spelled with integers and the gate passes.
Fixing the comparison without also canonicalising the write path would be worse
than the trap: `_checked_value` validates an outgoing value against the same set,
so the switch would exist and refuse every write.
Mutation evidence: restoring the silent `continue` fails 4 tests, emitting the
grid unconditionally fails 3, never emitting it fails 3.
…dline The whole setup used to run under one 60s cap. A cold sign-in alone is nine sequential round-trips, and each appliance adds more, so a working but slow account blew the cap and surfaced as a network timeout it never was. Budgets are now per phase and share one absolute deadline through a ContextVar stack of the live asyncio.Timeout scopes. A sign-in scope SUSPENDS the scopes it interrupts -- it pushes their deadline out on entry and gives the unused remainder back on exit -- so a lazy login nested inside a caller's scope can no longer be killed by the shorter budget wrapping it. Two kinds of number, kept apart on purpose: scope budgets (own work, suspendable) and caps awaited across the thread boundary on a concurrent.futures.Future (not suspendable, so each must contain a sign-in).
…ally ADDHON-400 could not tell a slow sign-in from a slow appliance list: the lazy login runs inside the load_appliances phase, so every login timeout was filed against the list. phase_timeout_code now resolves composed phases leaf first, so load_appliances/auth/refresh reports 406 and load_appliances/auth reports 405 while every flat phase resolves exactly as before. 480 covers the call that was still in flight when the dedicated loop was torn down. classify() gains a structural layer -- exception type and response status are read before the text of the message, which stays as the fallback it always should have been.
…'s deadline A single network blip during validation was a final error: the login path had no retry at all, while the runtime poll had three attempts. Retry is by explicit inclusion and covers only the five round-trips that can be replayed safely; the steps that mint an OTP or advance the Salesforce session are deliberately left out, so a retry can never multiply a verification code. The delay is fixed, not exponential. The gate measures the deadline of the scope actually in force rather than rebuilding one from AUTH_FULL, so a retry cannot be the reason its own budget expires. Each auth scope suspends its caller.
The watchdog read the phase after cancelling and draining the task, by which point the scope had already unwound and the mirror was empty, so it fell back to the flat phase and reported ADDHON-400 again. It now samples the phase before scheduling the cancellation, and keeps the real cause when the coroutine loses the race with the watchdog. Since 3.11 concurrent.futures.TimeoutError IS TimeoutError, so the cap expiry can no longer be told from a timeout raised BY the task by type. It is now told apart by state, through future.done(). A transport fault on one appliance no longer fails the whole entry: the appliance is queued for rehydration and reloaded before the first poll decides which entities exist, and a second failure is not contained a second time. Statistics move to the first poll, which reloads them anyway. MQTT startup gets the scope its budget was already summed into, and the wait no longer holds the lifecycle lock across the thread boundary.
Download Diagnostics now carries a per-phase ledger of duration and outcome, so the next report like #76 says which phase burned the time without needing a live probe. The log line used to read "Validation failed [ADDHON-400]: ADDHON-400: ..." -- the label was formatted next to an error whose message already began with it. The code appears once now, in the config flow and in the ConfigEntryNotReady and UpdateFailed messages alike.
There was a problem hiding this comment.
Sorry @tis24dev, your pull request is larger than the review limit of 150000 diff characters
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
📝 WalkthroughWalkthroughChangesSetup resilience and diagnostics
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideRefactors the hOn client’s timeout, phase attribution, and transport error handling to introduce per-phase budgets and hierarchical phases for better diagnostics and resilience (issue #76), adds model/catalogue metadata and richer diagnostics, tightens retry semantics, and updates tests and manifest for v5.12.0. Sequence diagram for lazy sign-in with hierarchical phases and shared budgetssequenceDiagram
participant HC as HonClient
participant NH as NativeHon
participant CN as HonConnection
participant HA as HonAuth
participant PM as BudgetModule
participant PT as PhaseTracker
participant RB as RetryBudget
HC->>NH: setup_sync()
NH->>NH: setup()
NH->>PT: PhaseTracker()
NH->>CN: create(phase_tracker)
CN->>HA: HonAuth(..., phase_tracker)
NH->>CN: request needing auth
CN->>PT: phase("auth", phase_tracker)
CN->>PM: budgeted(AUTH_FULL, suspends_caller=True)
activate PM
PM-->>RB: current_deadline()
RB-->>HA: RetryBudget(deadline)
HA->>HA: authenticate()
HA->>PT: _phase("introduce") -> step("introduce")
HA->>PM: retry_transport(budget, "introduce", _introduce)
HA->>PT: _phase("redirects") -> step("redirects")
HA->>PM: retry_transport(budget, "manual_redirect", _manual_redirect)
HA->>PT: _phase("login_page") -> step("login_page")
HA->>PM: retry_transport(budget, "login_page", _open_login_page)
HA->>PT: _phase("api_auth") -> step("api_auth")
HA->>PM: retry_transport(budget, "api_auth", _api_auth)
PM-->>CN: budgeted scope completes
deactivate PM
PT-->>CN: current_phase == "auth/..."
CN-->>NH: tokens ready
NH-->>HC: setup complete or coded timeout via HonCodedError
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
custom_components/addhon/hon_client.py (2)
801-808: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRead the session from one attribute, and fix the stale method name in the comment.
Two issues in this pair:
_needs_rehydrationreadsself._api, while the phase scope on Line 847 readsself._hon_instance. In production both hold the sameNativeHonobject, becauseNativeHon.__aenter__returnsselfandsetup_syncassigns that toself._api. The split is not a defect today, but it makes the rehydration path depend on two names for one object.tests/test_hon_client_realtime.pyLine 508 already sets only_api, so the test silently passestracker=None.- The comment on Line 836 names
NativeHon._build_appliance. The method incustom_components/addhon/client/session.pyis_create_appliance. A reader following the reference finds nothing.♻️ Proposed change
- Under the SAME scope pair `NativeHon._build_appliance` opens for + Under the SAME scope pair `NativeHon._create_appliance` opens forAlso applies to: 836-848
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@custom_components/addhon/hon_client.py` around lines 801 - 808, Update _needs_rehydration and the surrounding rehydration phase to use a single session attribute consistently, matching the attribute already initialized by setup and used by the tests, instead of mixing _api with _hon_instance. Correct the nearby comment to reference NativeHon._create_appliance rather than the stale _build_appliance name.
516-522: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueAfter a drain timeout,
drainedis read while the loop thread can still write it.
drain_future.result(timeout=self._CANCEL_TIMEOUT)establishes the happens-before for the normal path. If that wait expires, theexcept Exceptionon Line 519 swallows it and Line 522 readsdrainedwhile_drain_taskmay still be running on the loop thread. The dict operations are individually atomic, so nothing corrupts, but the recovered cause can be missed or read mid-handoff, and the attribution silently degrades to the synthetic phase timeout.The path is already degraded, so this is optional hardening. Capture the drain outcome in a local before the read, and record whether the drain completed.
♻️ Optional hardening
+ drain_completed = True try: loop.call_soon_threadsafe(_cancel_and_drain) drain_future.result(timeout=self._CANCEL_TIMEOUT) except Exception as err: + drain_completed = False _LOGGER.debug("Timeout while cancelling hOn task: %s", err) elapsed = round(time.monotonic() - started, 1) - original = drained.get("error") + original = drained.get("error") if drain_completed else None🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@custom_components/addhon/hon_client.py` around lines 516 - 522, Update the cancellation flow around _cancel_and_drain and drain_future.result to capture the drain wait outcome in a local variable and track whether draining completed, including when the timeout/exception path runs. Use that completion state when reading drained so the recovered error attribution does not assume the loop-thread handoff finished after a timeout.Source: Linters/SAST tools
tests/test_native_session.py (1)
608-608: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse an async stub for
_make_mqtt.
lambda hon: Nonereplaces anasync defmethod. This test passes only becausesetup()raises before it reachesawait self._make_mqtt(). The same pattern appears on Lines 633 and 676. If the abort condition ever changes, the await fails with "object NoneType can't be used in 'await' expression" and the test reports the wrong cause. Line 710 in this file already uses the correct form.♻️ Proposed change (apply to Lines 608, 633, and 676)
+ async def no_mqtt(hon): + return None + self._patch(factory, "create_appliance", fake_create_appliance) - self._patch(NativeHon, "_make_mqtt", lambda hon: None) + self._patch(NativeHon, "_make_mqtt", no_mqtt)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_native_session.py` at line 608, Replace the synchronous lambda stubs for NativeHon._make_mqtt in the tests at the referenced setup points with async stubs matching the existing pattern used near line 710. Update all three occurrences, including those around lines 608, 633, and 676, so awaiting _make_mqtt remains valid if setup proceeds.tests/test_hon_client_realtime.py (1)
177-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueClose the event loop in the cleanup.
_stop_hon_loopcloses the loop in production, but this helper only stops it and joins the thread. Six tests in this class use the helper, so the run leaks one unclosed loop each and can emitResourceWarning: unclosed event loop.♻️ Proposed cleanup
def _stop() -> None: loop = client._hon_loop if loop is not None: loop.call_soon_threadsafe(loop.stop) thread = client._hon_thread if thread is not None: thread.join(timeout=5) + if loop is not None and not loop.is_closed(): + loop.close()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_hon_client_realtime.py` around lines 177 - 190, Update the _client cleanup helper’s _stop function to close the client’s event loop after stopping it and joining the thread, matching _stop_hon_loop’s production cleanup behavior. Ensure the loop is closed only when it exists and preserve the existing thread shutdown flow.tests/test_auth_retry_policy.py (1)
159-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNarrow the expected exception type.
assertRaises(BaseException)passes on any failure, including anAttributeErrororImportErrorfrom a broken double. This file states that the non-regression half is the valuable half, so the assertion should pin the injected error. All three callers injectasyncio.TimeoutError, and every non-retried step lets it propagate. The call-count assertion below still guards the delivery-once rule.♻️ Proposed change
def _step_is_delivered_once(self, step: str) -> None: index = _STEP_INDEX[step] session = _FlakySession(_happy_responses(), index, asyncio.TimeoutError()) auth = _auth(session) - with self.assertRaises(BaseException): + with self.assertRaises(asyncio.TimeoutError): asyncio.run(auth.authenticate())🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_auth_retry_policy.py` around lines 159 - 167, Update _step_is_delivered_once to assert asyncio.TimeoutError specifically instead of BaseException, while preserving the existing authenticate invocation, call-count verification, and sleeper-delay assertion.Source: Linters/SAST tools
tests/test_setup_budgets.py (1)
663-683: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winWiden the timing margin in this test.
The suspended deadlines land at about 0.85s and 0.80s against about 0.70s of elapsed time. That leaves a margin of roughly 0.10s. A loaded CI runner can exceed it and fail the test for a scheduling delay rather than a stack regression. The sibling tests in this class keep a margin of about 2x. Scale the five numbers up so the margin matches, or express the assertion against a factor of the sleeps.
♻️ Proposed change
- # 0.60 outer / 0.55 middle; a sign-in budgeted 0.30 that really takes 0.25, then - # 0.45s of the caller's own work. Suspended properly both enclosing deadlines end - # at ~0.85/0.80 against 0.70 elapsed; with only the parent suspended the outer one - # still sits at 0.60 and fires. - async with budgeted(0.60): + # 1.20 outer / 1.10 middle; a sign-in budgeted 0.60 that really takes 0.50, then + # 0.90s of the caller's own work. Suspended properly both enclosing deadlines end + # at ~1.70/1.60 against 1.40 elapsed; with only the parent suspended the outer one + # still sits at 1.20 and fires. + async with budgeted(1.20): self.assertEqual(1, len(budget._ACTIVE.get())) - async with budgeted(0.55): + async with budgeted(1.10): # The shape, stated directly: the enclosing scope is still on the stack. self.assertEqual(2, len(budget._ACTIVE.get())) - async with budgeted(0.30, suspends_caller=True): - await asyncio.sleep(0.25) - await asyncio.sleep(0.45) + async with budgeted(0.60, suspends_caller=True): + await asyncio.sleep(0.50) + await asyncio.sleep(0.90)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_setup_budgets.py` around lines 663 - 683, Widen the timing margin in test_a_sign_in_suspends_every_enclosing_scope_not_just_its_parent by scaling the five budget and sleep durations proportionally, preserving the same ordering and suspension behavior while increasing the gap between the expected suspended deadlines and elapsed runtime to roughly 2x.custom_components/addhon/client/transport/mqtt.py (1)
210-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLog the suppressed failures instead of passing silently.
Both blocks swallow every exception. If
hon._phase_trackeris missing orstep()fails, the MQTT connect/subscribe refinement is lost and no line records it. That is the unfalsifiable-report class this PR removes elsewhere. Add a debug log to each handler. This also clears the Ruff S110/BLE001 findings.♻️ Proposed change
try: hon._setup_phase = phase - except Exception: # pragma: no cover - defensive - pass + except Exception: # noqa: BLE001 - defensive, never fatal + _LOGGER.debug("MQTT: could not record the flat setup phase", exc_info=True) try: # The HIERARCHICAL mirror too (client/phase.py). `NativeHon.setup()` # wraps this call in a `phase("mqtt_start")` scope for the MQTT_START # budget, and that scope is what the cross-thread watchdog reads -- # without the refinement the outer name would SHIELD the flat mirror # above and cost the connect/subscribe distinction. `step()` only # rewrites the mirror (no ContextVar push), and the enclosing scope # restores it on exit, so a refinement cannot outlive the start. hon._phase_tracker.step(phase) - except Exception: # pragma: no cover - defensive - pass + except Exception: # noqa: BLE001 - defensive, never fatal + _LOGGER.debug("MQTT: could not refine the hierarchical phase mirror", exc_info=True)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@custom_components/addhon/client/transport/mqtt.py` around lines 210 - 224, Update both defensive exception handlers around hon._setup_phase and hon._phase_tracker.step(phase) to log the caught exception at debug level instead of silently passing; include enough context to identify whether updating the setup phase or phase tracker failed, while preserving the existing best-effort flow.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@custom_components/addhon/hon_client.py`:
- Around line 1145-1153: Update the all-failed HonCodedError raise in
async_get_appliances_data to forward phase=getattr(cause, "phase", None) from
the _representative_failure result, matching the existing first-poll wrapper.
Preserve the existing error code, message, and cause chaining.
In `@tests/test_hon_client_realtime.py`:
- Around line 323-378: Correct
test_a_teardown_gives_the_in_flight_caller_a_coded_error by removing or revising
the assertion that the concurrent.futures.CancelledError cause is not an
asyncio.CancelledError. Since both names resolve to the same class on supported
Python versions, assert the expected cancellation type without claiming they are
distinct, while preserving the HonCodedError and CLIENT_SHUTDOWN checks.
---
Nitpick comments:
In `@custom_components/addhon/client/transport/mqtt.py`:
- Around line 210-224: Update both defensive exception handlers around
hon._setup_phase and hon._phase_tracker.step(phase) to log the caught exception
at debug level instead of silently passing; include enough context to identify
whether updating the setup phase or phase tracker failed, while preserving the
existing best-effort flow.
In `@custom_components/addhon/hon_client.py`:
- Around line 801-808: Update _needs_rehydration and the surrounding rehydration
phase to use a single session attribute consistently, matching the attribute
already initialized by setup and used by the tests, instead of mixing _api with
_hon_instance. Correct the nearby comment to reference
NativeHon._create_appliance rather than the stale _build_appliance name.
- Around line 516-522: Update the cancellation flow around _cancel_and_drain and
drain_future.result to capture the drain wait outcome in a local variable and
track whether draining completed, including when the timeout/exception path
runs. Use that completion state when reading drained so the recovered error
attribution does not assume the loop-thread handoff finished after a timeout.
In `@tests/test_auth_retry_policy.py`:
- Around line 159-167: Update _step_is_delivered_once to assert
asyncio.TimeoutError specifically instead of BaseException, while preserving the
existing authenticate invocation, call-count verification, and sleeper-delay
assertion.
In `@tests/test_hon_client_realtime.py`:
- Around line 177-190: Update the _client cleanup helper’s _stop function to
close the client’s event loop after stopping it and joining the thread, matching
_stop_hon_loop’s production cleanup behavior. Ensure the loop is closed only
when it exists and preserve the existing thread shutdown flow.
In `@tests/test_native_session.py`:
- Line 608: Replace the synchronous lambda stubs for NativeHon._make_mqtt in the
tests at the referenced setup points with async stubs matching the existing
pattern used near line 710. Update all three occurrences, including those around
lines 608, 633, and 676, so awaiting _make_mqtt remains valid if setup proceeds.
In `@tests/test_setup_budgets.py`:
- Around line 663-683: Widen the timing margin in
test_a_sign_in_suspends_every_enclosing_scope_not_just_its_parent by scaling the
five budget and sleep durations proportionally, preserving the same ordering and
suspension behavior while increasing the gap between the expected suspended
deadlines and elapsed runtime to roughly 2x.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 0ec20790-6853-4ef3-b5b7-dda46ae37457
📒 Files selected for processing (32)
custom_components/addhon/__init__.pycustom_components/addhon/client/budget.pycustom_components/addhon/client/engine/appliance.pycustom_components/addhon/client/phase.pycustom_components/addhon/client/session.pycustom_components/addhon/client/transport/auth.pycustom_components/addhon/client/transport/connection.pycustom_components/addhon/client/transport/mqtt.pycustom_components/addhon/client/transport/retry.pycustom_components/addhon/config_flow.pycustom_components/addhon/diagnostics.pycustom_components/addhon/error_codes.pycustom_components/addhon/hon_client.pycustom_components/addhon/manifest.jsoncustom_components/addhon/switch.pycustom_components/addhon/translations/en.jsoncustom_components/addhon/translations/it.jsontests/_aiohttp_contract.pytests/test_air_purifier_entities.pytests/test_auth_error_classification.pytests/test_auth_retry_policy.pytests/test_config_flow_error_codes.pytests/test_coordinator_resilience.pytests/test_diagnostics.pytests/test_engine_appliance_root.pytests/test_error_codes.pytests/test_hon_client_realtime.pytests/test_native_session.pytests/test_phase_context.pytests/test_program_select.pytests/test_setup_budgets.pytests/test_transport_mqtt.py
…pper A steady-state cycle where every appliance fails wraps the representative cause in a HonCodedError, and __init__.py reads `phase` off THAT object without ever walking the __cause__ chain. The wrapper passed no phase, so Download Diagnostics filed phase=null for causes that knew exactly where they died -- the first-poll twin already forwarded it. Also read the phase tracker from the same session attribute the rehydration guard asks (both names hold the one NativeHon), and correct a comment that pointed at NativeHon._build_appliance, which does not exist; the method is _create_appliance.
Automated release PR for
v5.12.0.Summary by Sourcery
Introduce per-phase budgeting, hierarchical phase tracking, and improved error attribution for hOn client setup, authentication, appliance hydration, and diagnostics, addressing timeout misclassification and enhancing debuggability (issue #76), and bump the integration to v5.12.0.
Enhancements:
Tests:
Chores:
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Release
Greptile Summary
The release introduces phase-specific timeout budgets and attribution, bounded authentication retries, degraded-appliance recovery, and richer diagnostics and model metadata.
Confidence Score: 5/5
The pull request appears safe to merge.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Home Assistant setup or poll] --> B[HonClient dedicated loop] B --> C[Phase scope and workload budget] C --> D{Authentication needed?} D -- Yes --> E[Suspend caller budget] E --> F[Refresh or full authentication] F --> G[Resume caller budget] D -- No --> H[Cloud request] G --> H H --> I{Appliance hydration succeeds?} I -- Yes --> J[Poll and expose entities] I -- Temporary failure --> K[Record degraded appliance] K --> L[Rehydrate commands on first poll] L --> J I -- All retryable failures --> M[Raise representative coded failure] C --> N{Budget or watchdog expires?} N -- Yes --> O[Attribute timeout to active phase]Reviews (2): Last reviewed commit: "fix(diagnostics): forward the phase thro..." | Re-trigger Greptile