Skip to content

fix(observability): classify web-channel empty-provider-response re-report as expected (Sentry TAURI-RUST-4Z1)#2808

Merged
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/sentry-tauri-rust-4z1-empty-response-web-channel
May 28, 2026
Merged

fix(observability): classify web-channel empty-provider-response re-report as expected (Sentry TAURI-RUST-4Z1)#2808
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/sentry-tauri-rust-4z1-empty-response-web-channel

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 28, 2026

Summary

  • Add ExpectedErrorKind::EmptyProviderResponse + is_empty_provider_response_message predicate (anchored on "model returned an empty response") in src/core/observability.rs. The web_channel.run_chat_task re-report of an empty-provider-response now routes through report_expected_messagetracing::warn! (breadcrumb only) instead of a Sentry error event.
  • Targets self-hosted Sentry TAURI-RUST-4Z1 (run_chat_task failed … error=The model returned an empty response. Please try again., channel=web domain=web_channel operation=run_chat_task, on openhuman@0.56.0+e8968077aeb5).
  • Complements PR fix(agent): suppress empty-provider-response from Sentry (TAURI-RUST-4JX) #2790 (TAURI-RUST-4JX), which suppresses the agent-layer event for the same condition but cannot reach the web-channel re-report.

Problem

When the provider/model completes a turn with a completely empty body (text_chars=0 thinking_chars=0 tool_calls=0), the agent harness at src/openhuman/agent/harness/session/turn.rs:806 bails with the user-facing string:

The model returned an empty response. Please try again.

This is a model / user-config condition — a quirky or broken local fine-tune that returns nothing, or a provider that dropped the stream — not a code bug. The same failure surfaces at two emit sites:

Sentry ID Layer Emit site Suppression
TAURI-RUST-4JX agent agent::run_single PR #2790 — typed AgentError::EmptyProviderResponse + AgentError::skips_sentry()
TAURI-RUST-4Z1 (this PR) web_channel channels::providers::web::run_chat_task ❌ none until now

run_chat_task (src/openhuman/channels/providers/web.rs:1017) re-reports the failure under domain=web_channel after the typed AgentError was flattened to a String at the native-bus boundary (the file's own comment notes this: "Substring match is required here because the typed AgentError was flattened to a String"). Because the type is gone, PR #2790's skips_sentry() can't reach it, and observability.rs had no string classifier for the condition — so it escaped as a fresh Sentry event.

This is the same two-emit-site shape as the embedding session-expired fix (4P0 agent-layer + 4K5 embedding-layer), and it mirrors how MaxIterationsExceeded is already suppressed at both the agent layer and the web_channel report_error_or_expected funnel.

Solution

A string classifier in observability.rs — the documented mechanism for catching the same error re-reported at a higher layer under a different domain tag:

fn is_empty_provider_response_message(lower: &str) -> bool {
    lower.contains("model returned an empty response")
}

Dispatched last in expected_error_kind (so a more specific matcher always wins) and demoted to tracing::warn! — same tier as MaxIterationsExceeded, the other deterministic agent-state outcome surfaced to the user via the chat_error event.

Polarity contract — anchored on "model returned an empty response", NOT the looser "empty response", so the two internal fall-through paths stay actionable:

  • payload_summarizer.rs:261"summarizer returned empty response, falling through" (internal fall-through, not a failure).
  • subagent_runner/extract_tool.rs:379"provider returned an empty response; returning empty extraction" (graceful empty extraction).

Both use different subjects (summarizer / provider) and must keep reaching Sentry if they ever regress — pinned by the rejection test.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy — 2 new tests in src/core/observability.rs:
    • classifies_empty_provider_response_web_channel_rereport — verbatim TAURI-RUST-4Z1 run_chat_task wire shape + the bare user-facing string.
    • does_not_classify_unrelated_empty_response_phrases — 3-case rejection contract (summarizer fall-through, extract-tool graceful empty, generic health-probe empty body).
  • Diff coverage ≥ 80% — every new line (variant, predicate, dispatcher arm, demote arm) is exercised by the new tests. cargo test --lib core::observability::tests → 94 passed (2 new).
  • N/A: Coverage matrix updated — observability classifier change is behaviour-only inside the core::observability module; not a tracked feature row in docs/TEST-COVERAGE-MATRIX.md.
  • N/A: All affected feature IDs from the matrix are listed in the PR description under ## Related — no matrix feature IDs affected.
  • No new external network dependencies introduced — pure in-process classifier addition.
  • N/A: Manual smoke checklist updated — observability classifier; no user-visible UI surface, no release-cut behaviour change.
  • N/A: Linked issue closed via Closes #NNN — Sentry-only fix; no GitHub issue. The Sentry-Issue trailer below carries the back-reference.

Impact

  • Runtime: Desktop (Tauri shell) + core. The web_channel.run_chat_task empty-response re-report now classifies as EmptyProviderResponse, demoting to tracing::warn! (no Sentry event). No behaviour change to the chat flow, the chat_error event the user sees, or the AgentError semantics.
  • Performance: Net positive — removes the TAURI-RUST-4Z1 event stream that PR fix(agent): suppress empty-provider-response from Sentry (TAURI-RUST-4JX) #2790 leaves uncovered.
  • Security: None — no new code paths, no PII (the breadcrumb retains the message but it carries only client/thread/request IDs, no payload).
  • Migration / compatibility: None — additive enum variant + additive classifier + additive dispatcher arm.
  • Observability trade-off: The local tracing::warn! breadcrumb retains the full message (client/thread/request IDs) for correlation; only the Sentry capture is suppressed. A genuine regression in the summarizer / extract-tool fall-through paths still reaches Sentry (different wording, not matched).

Notes for reviewers

Related

  • Sentry-Issue: TAURI-RUST-4Z1
  • Complements: PR fix(agent): suppress empty-provider-response from Sentry (TAURI-RUST-4JX) #2790 (TAURI-RUST-4JX — agent-layer suppression of the same condition).
  • Agent emit site: src/openhuman/agent/harness/session/turn.rs:806. Web-channel re-report site: src/openhuman/channels/providers/web.rs:1017.
  • Precedent: MaxIterationsExceeded (suppressed at both the agent layer and the web_channel funnel) and the 4P0/4K5 embedding session-expired two-arm fix.

Summary by CodeRabbit

  • Bug Fixes
    • Improved classification and handling of empty model/provider responses so repeated re-reports are demoted to warnings instead of error events.
    • Ensures the canonical empty-response message is consistently recognized while avoiding false positives for unrelated phrases.
    • Added unit tests to verify correct classification and suppression behavior.

Review Change Stack

@CodeGhost21 CodeGhost21 requested a review from a team May 28, 2026 04:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Warning

Review limit reached

@M3gA-Mind, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 54 minutes and 45 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bf22dc5-d3b6-4df5-8191-3c1e27e110ba

📥 Commits

Reviewing files that changed from the base of the PR and between 0858e59 and e33102a.

📒 Files selected for processing (1)
  • src/core/observability.rs
📝 Walkthrough

Walkthrough

Adds ExpectedErrorKind::EmptyProviderResponse, a matcher is_empty_provider_response_message, extends expected_error_kind to use it, updates report_expected_message to demote this kind to a breadcrumb, and adds unit tests for classification and non-matching phrases.

Changes

Empty Provider Response Error Classification

Layer / File(s) Summary
Error kind definition
src/core/observability.rs
ExpectedErrorKind::EmptyProviderResponse enum variant is added with documentation explaining the agent-layer typed suppression vs. web-channel re-report emit sites.
Classification logic
src/core/observability.rs
is_empty_provider_response_message predicate recognizes the anchored "model returned an empty response" phrase, and expected_error_kind is extended to invoke this matcher after the PromptInjectionBlocked arm.
Reporting behavior
src/core/observability.rs
report_expected_message handles ExpectedErrorKind::EmptyProviderResponse by emitting a tracing::warn! breadcrumb with kind="empty_provider_response".
Test coverage
src/core/observability.rs
Unit tests validate web-channel re-report strings (wrapped and bare) classify as EmptyProviderResponse and that unrelated "empty response" phrases are not misclassified.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2830: Modifies src/core/observability.rs and extends ExpectedErrorKind / classifier/reporting paths (overlaps on classifier/reporting code paths).
  • tinyhumansai/openhuman#2790: Also introduces handling for the "empty provider response" condition; overlaps on the same error path.
  • tinyhumansai/openhuman#1795: Adds a different ExpectedErrorKind and matcher in the same observability classification/reporting pipeline.

Suggested labels

rust-core, agent, sentry-traced-bug

Suggested reviewers

  • graycyrus
  • oxoxDev

Poem

🐰 I sniffed the silence, soft and small,
A model's hush — no urgent call.
I tuck the whisper into breadcrumbs light,
So sentries rest through calm night.
Hooray for quiet, gentle sight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: adding a new expected error classification for empty-provider-response re-reports in the web-channel observability path, directly matching the core intent of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 28, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 28, 2026
graycyrus
graycyrus previously approved these changes May 28, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Clean fix. The two-emit-site pattern is well-understood here (same shape as MaxIterationsExceeded and the 4P0/4K5 embedding session-expired pair), and the implementation is exactly right.

The predicate anchors on "model returned an empty response" rather than the looser "empty response", which keeps the summarizer fall-through and extract-tool graceful-empty paths actionable. The rejection test covers all three of those cases explicitly. Dispatcher arm runs last, documentation is thorough, tracing fields are consistent with sibling variants.

CI is fully green — Rust Quality, Core Coverage, Tauri Coverage, and the Coverage Gate all passed. LGTM.

@graycyrus graycyrus dismissed their stale review May 28, 2026 07:35

Auto-corrected: reviewer policy violation — clean PRs must go to to-be-approved/, never auto-APPROVE

Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev left a comment

Choose a reason for hiding this comment

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

Author: @CodeGhost21 (MEMBER — core team)

Core fix sound — anchor "model returned an empty response" correctly distinguishes the user-facing bail at turn.rs:815 from sibling non-failure phrasings; dispatcher placement last in expected_error_kind is right; demote to tracing::warn! consistent with MaxIterationsExceeded.

Two forward-compat improvements (not blockers):

Nitpick 1 — src/core/observability.rs:743 — rejection contract narrower than actual sibling surface

Grepping src/ for "empty response" surfaces 3 more legitimate non-failure phrasings that should be in the rejection test for forward-compat — if anyone ever loosens the anchor to "empty response", these would silently stop reaching Sentry:

  • src/openhuman/channels/bus.rs:185"[channel-inbound] agent returned empty response — finalizing draft with fallback" (channel-inbound graceful fallback; routes through report_error_or_expected)
  • src/openhuman/memory/query/walk.rs:292"[memory_tree_walk] turn={turn} LLM gave up (empty response)"
  • src/openhuman/learning/reflection.rs:576"[learning] reflection skipped (empty response — gate off or local AI unavailable)"

Boundary case worth pinning too: src/openhuman/agent/harness/session/turn.rs:811 uses "provider returned an empty final response" — different subject (provider vs model), MUST NOT classify. Worth an explicit assert in the same rejection test.

Suggested addition:

for raw in [
    // existing entries…
    "[channel-inbound] agent returned empty response — finalizing draft with fallback",
    "[memory_tree_walk] turn=3 LLM gave up (empty response)",
    "[learning] reflection skipped (empty response — gate off or local AI unavailable)",
    "[agent_loop] provider returned an empty final response (i=2, no text, no tool calls)",
] {
    assert_eq!(expected_error_kind(raw), None, "must NOT classify: {raw}");
}

Nitpick 2 — variant doc-comment understates cross-channel coverage

PR title + variant doc-comment frame this as a web_channel.run_chat_task re-report fix. But the classifier runs in the central expected_error_kind dispatcher — it also catches matching messages routed through channels/runtime/dispatch.rs:1075 and channels/runtime/supervision.rs:61. Any channel provider (slack / discord / whatsapp / etc) that re-routes through the central funnel now demotes too. That's the desired outcome but worth an explicit doc note so a future reader doesn't re-introduce per-channel typed suppression.

Append to variant doc:

/// Although the immediate trigger is the web-channel re-report, this
/// classifier runs in the central `expected_error_kind` dispatcher, so any
/// caller of `report_error_or_expected` (channels/runtime/dispatch.rs,
/// channels/runtime/supervision.rs, any future channel provider) whose
/// error chain contains "model returned an empty response" is also
/// demoted — no per-channel typed suppression needed.

Question — graycyrus DISMISSED on same SHA

gh api repos/.../pulls/2808/reviews shows graycyrus APPROVED then DISMISSED at the same commit SHA (0858e59e). Worth a quick ping to confirm dismissal was intentional vs UI mishap before merging.

CI 30/30 green. mergeStateStatus=BLOCKED is review-gate per [[feedback_pr_blocked_review_vs_ci]], not CI. Per the nits being non-blocking, leaving this as COMMENT — happy to flip to APPROVE once either nit lands or you confirm to skip them.

M3gA-Mind
M3gA-Mind previously approved these changes May 28, 2026
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind left a comment

Choose a reason for hiding this comment

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

PR #2808 — fix(observability): classify web-channel empty-provider-response re-report as expected

Walkthrough

Correct two-emit-site fix. The agent-layer suppression (PR #2790, AgentError::skips_sentry()) can't reach the web_channel.run_chat_task re-report because the typed error was flattened to a String at the native-bus boundary. Adding EmptyProviderResponse to the central expected_error_kind dispatcher closes that second emit site — the exact same pattern as MaxIterationsExceeded and the 4P0/4K5 embedding session-expired pair.

Changes

File Summary
src/core/observability.rs Add EmptyProviderResponse variant, is_empty_provider_response_message predicate, dispatcher arm, demote handler, 2 tests; expand rejection test with 4 more sibling phrases; add cross-channel coverage note to variant doc

Actionable comments (0)

No blockers or major issues.

Verified / looks good

  • Anchor "model returned an empty response" correctly excludes "summarizer returned empty response", "provider returned an empty response; returning empty extraction", "provider returned an empty final response", "agent returned empty response", and two other graceful-empty phrases — all now pinned by the rejection test.
  • Dispatcher placement last in expected_error_kind means any more-specific matcher always wins.
  • tracing::warn! demotion consistent with MaxIterationsExceeded and other deterministic agent-state outcomes.
  • Variant doc now notes that the classifier runs in the central dispatcher and covers all channel providers (channels/runtime/dispatch.rs, supervision.rs), not just web_channel.run_chat_task.
  • All CI checks pass (30/30).

CodeGhost21 and others added 2 commits May 29, 2026 00:47
…eport as expected (Sentry TAURI-RUST-4Z1)

Add `ExpectedErrorKind::EmptyProviderResponse` + `is_empty_provider_response_message`
predicate anchored on the user-facing string `"model returned an empty
response"`. Routes the `web_channel.run_chat_task` re-report through the
demote path (`tracing::warn!`, breadcrumb only) instead of a Sentry
error event.

Root cause: when the provider/model returns a completely empty body
(`text_chars=0 thinking_chars=0 tool_calls=0`), the agent harness
(`agent::harness::session::turn`) bails with the user-facing
`"The model returned an empty response. Please try again."` string.
PR tinyhumansai#2790 (TAURI-RUST-4JX) suppresses the AGENT-layer Sentry event via
the typed `AgentError::EmptyProviderResponse` + `skips_sentry()`. But
`channels::providers::web::run_chat_task` RE-REPORTS the same failure
under `domain=web_channel operation=run_chat_task` after the typed
error was flattened to a `String` at the native-bus boundary — so the
typed suppression can't reach it and it escapes as a fresh Sentry event
(TAURI-RUST-4Z1). Same two-emit-site pattern as the embedding 4P0/4K5
session-expired fix; mirrors how `MaxIterationsExceeded` is suppressed
at both the agent layer and the web_channel `report_error_or_expected`
funnel.

Anchored on `"model returned an empty response"` (not the looser
`"empty response"`) so the internal fall-through paths stay actionable:
`payload_summarizer` (`"summarizer returned empty response, falling
through"`) and `subagent_runner::extract_tool` (`"provider returned an
empty response; returning empty extraction"`) use different subjects
and are NOT silenced — pinned by the rejection test.

3 new tests: web-channel wire shape (verbatim TAURI-RUST-4Z1 body) +
bare user-facing string, and a 3-case rejection contract (summarizer
fall-through, extract-tool graceful empty, generic health-probe empty
body). cargo test --lib core::observability::tests -> 94 passed.

Sentry-Issue: TAURI-RUST-4Z1
…oc-comment

- Add 4 more sibling phrases to the rejection test (channel-inbound
  graceful fallback, memory walk, reflection skip, and turn.rs
  "provider returned an empty final response") so any future anchor
  loosening can't silently stop those paths reaching Sentry.
- Expand the EmptyProviderResponse variant doc to note that the
  classifier runs in the central dispatcher and covers all channel
  providers, not just web_channel.run_chat_task.

Addresses nitpicks from code review.
@M3gA-Mind M3gA-Mind force-pushed the fix/sentry-tauri-rust-4z1-empty-response-web-channel branch from b434802 to e33102a Compare May 28, 2026 19:17
@M3gA-Mind M3gA-Mind merged commit 72cdfe9 into tinyhumansai:main May 28, 2026
33 of 57 checks passed
@coderabbitai coderabbitai Bot added rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. sentry-traced-bug Bug identified via Sentry triage labels May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. sentry-traced-bug Bug identified via Sentry triage working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants