Repository navigation
fix(grid-status): stop the chat bot reporting a stale VRM reading as live status - #201
Merged
Merged
Conversation
…live status customer_get_grid_status read battery SOC/current, solar power, and grid consumption straight off VRM with no staleness gate, unlike inverter power (gated since the earlier /grids fix). VRM keeps serving the last sample after a gateway goes dark, so a site with no comms for hours could still return a concrete-looking "75%, discharging" reading sitting right next to a correctly computed service: "Unknown" / is_stale: true. Nothing in the prompt told the model to trust the flag over the numbers, and the one tool that could explain the contradiction (an active inverter alarm) was staff-only, so the bot told a customer their grid was fine off data many hours old. - live_metrics_view() gates battery/solar/consumption on the same voltage-reading staleness inverter_power_view already uses, since all four readings come from one site's gateway in one request. - customer.system.prompt gets a "Stale or Unknown Live Data" section: don't state stale numbers as current fact, check get_recent_production_errors_and_power for an active alarm first, escalate if that doesn't explain it either. - get_recent_production_errors_and_power is now visible_to_customer, so the bot can actually see alarm data in a customer conversation. It had no organization scoping of its own (safe only while staff-only) -- added the same STAFF_ORG_ID/organization_id filtering get_grid_status already has, and made the MCP dispatcher require organization_id rather than falling through to the client method's unscoped default (kept for the internal get_live_telemetry alert-judgment caller). Pre-existing, unrelated local-env failures (PIL missing) in 3 knowledge_server tests are untouched by this change -- confirmed by reproducing them on main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A customer asked the Telegram bot for their grid's status while its gateway had been dark for ~17 hours (an active inverter overload alarm, per VRM). The bot replied twice, confidently, that the grid was "currently powered ... battery at 75%," attributing the "unable to monitor" banner purely to a DCU/comms issue.
Three compounding gaps, all in the customer status path:
customer_get_grid_statushad no staleness gate on battery/solar/consumption. Inverter power was already gated (an earlier/gridsfix), butbattery_soc_pct/battery_current_a/solar_power_w/grid_consumption_wwere read straight off VRM every time — and VRM keeps serving the last sample indefinitely after a gateway goes dark. The tool's ownservice_statuscorrectly said"Unknown"/is_stale: true, but that sat right next to a concrete-looking, actually-frozen battery reading.is_stale/"Unknown"meant. Zero mentions of either anywhere in the prompt library, so the model kept the punchy number and downgraded the stale flag to a footnote.get_recent_production_errors_and_power(VE.Bus alarms/power history) was staff-only.Changes
live_metrics_view()(client_grid_status.py) gates battery/solar/consumption on the same voltage-reading stalenessinverter_power_viewalready uses — all four readings come from one site's gateway in one request, so a stale/missing/errored voltage reading is the correct shared proxy.customer.system.promptgets a new "Stale or Unknown Live Data" section: don't state a stale reading as current fact, check the alarm tool first, escalate per the existing Uncertainty policy if that still doesn't explain it.get_recent_production_errors_and_poweris now customer-visible. It had no organization scoping of its own — safe only while staff-only. Added the sameSTAFF_ORG_ID/organization_idfilteringget_grid_statusalready uses, and made the MCP dispatcher requireorganization_id(erroring if missing, matching every sibling customer tool) rather than falling through to the client method's unscoped default — which stays as the default specifically for the internalget_live_telemetryalert-judgment caller, which has never passed one.Test plan
test_grid_status_stale_battery.py—live_metrics_viewblanks battery/solar/consumption on stale/missing/errored/exception voltage readings, keeps fresh readings (including genuine zeros).test_production_errors_org_scoping.py— customer lookups are scoped to their own org; staff and the internal alert-judgment caller (noorganization_id) stay unscoped; the MCP dispatcher rejects a call with noorganization_id.PYTHONPATH="$PWD:$PWD/mcp_servers" pytest mcp_servers/tests -q→ 785 passed. The only 3 failures (knowledge_server, Google Sheets doc-editing) are a pre-existing localPILimport gap — reproduced identically onmainbefore this change, untouched by this diff.mcp_servers/tool_definitions.jsonviaexport_tools.py(108 tools, 0 errors) — diff is exactly the two description edits + the visibility flip.chat_orchestrator/tests/prompt_checksums.json— diff is exactly thecustomer.systemhash.pre-commit run --all-filesclean.🤖 Generated with Claude Code