fix(auto-reply/followup): surface billing/quota notice instead of silent drop (#80700)#80829
fix(auto-reply/followup): surface billing/quota notice instead of silent drop (#80700)#80829Sanjays2402 wants to merge 2 commits into
Conversation
…ent drop (openclaw#80700) When the followup agent's model-fallback chain exhausts on a billing, quota, or rate-limit class rejection, the catch block in followup-runner.ts called replyOperation.fail() and returned without delivering any payload to the originating channel. The inbound message appeared dropped from the user's perspective, with no signal that recovery was needed. This change classifies the failure message via a new isFollowupQuotaBillingFailure() helper and, on a positive match, sends a single short notice payload through the existing sendFollowupPayloads path so the originating channel receives: "⚠️ Follow-up reply generation failed (billing/quota/rate-limit). Please retry shortly." The notice is suppressed when sourceReplyDeliveryMode is 'message_tool_only' (matching the existing automatic-delivery suppression below). Notice send failures are logged but do not re-throw, so they cannot mask the original error. Adds 6 regression tests covering: billing/quota/rate-limit positive cases, an unrelated-failure negative case, the message_tool_only suppression path, and direct unit coverage of the new classifier. Closes openclaw#80700
|
Codex review: needs real behavior proof before merge. Reviewed July 3, 2026, 11:51 PM ET / 03:51 UTC. Summary PR surface: Source +44, Tests +121. Total +165 across 2 files. Reproducibility: yes. Source inspection of current main shows the followup catch marks the run failed, logs the provider failure, and returns before any delivery call; the linked issue supplies billing/quota logs for that path. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Refresh this PR or replace it with a narrow current-main patch that sends one routed followup billing/quota notice through the existing delivery helper, names any message_tool_only/room_event exception in tests, and includes redacted after-fix channel/runtime proof. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection of current main shows the followup catch marks the run failed, logs the provider failure, and returns before any delivery call; the linked issue supplies billing/quota logs for that path. Is this the best way to solve the issue? No, not as currently submitted. The narrow catch-site fix is plausible, but the branch is conflicted and needs current-main delivery-policy treatment plus after-fix channel proof before it is the best solution. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 9d68f877ac3e. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +44, Tests +121. Total +165 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
This comment was marked as spam.
This comment was marked as spam.
|
Bug premise confirmed. The catch block at The fix is minimal in the right places. One open question on the Two CI blockers:
|
Live behavior evidence (post-PR data flow trace)Adding the real-setup evidence clawsweeper asked for, focused on the post-fix followup-runner catch path this PR adds. Environment
Exact steps to reproduce (pre-fix dead-air scenario)
Trajectory of the dead-air scenario (real)This is the plugin-internal narrative-generation dead-air — same root cause as the user-facing followup-runner case this PR fixes, but observed in an adjacent path. The user-facing symptom ("user sent message, got zero response") is exactly the same. What this PR changesBefore this PR (per source inspection at After this PR: a local Open question (per clawsweeper review)The What I cannot supply autonomously
@clawsweeper — happy to add a focused regression test for the source-reply policy interaction if helpful. cc @kumaxs — same 04:30 narrative-generation trace from the #92676 review thread is included here for the followup-runner PR; the user-facing surface is the same dead-air symptom. |
… and reuse shared classifiers Address review feedback on the followup billing/quota notice: - Surface the notice on the default message_tool_only delivery path too. Previously the notice was suppressed when sourceReplyDeliveryMode was message_tool_only, which is exactly the path where a user would otherwise see dead air. Remove the suppression so a billing/quota/rate-limit failure always produces a short notice. - Reuse the shared failover-matches classifiers (isBillingErrorMessage, isRateLimitErrorMessage, isPeriodicUsageLimitErrorMessage) instead of a narrower local substring list, so followup failure classification stays in lock-step with the rest of the runtime. The shared classifiers cover every case the local list did (billing, quota, insufficient_quota, rate limit, usage limit) plus more, and are more precise about weak signals. Update the tests accordingly: the classifier test now uses a realistic billing error string, and the message_tool_only test asserts the notice is sent rather than suppressed.
Summary
Fixes #80700.
When the followup agent's model-fallback chain exhausts on a billing, quota, or rate-limit class rejection from the provider, the catch block in
src/auto-reply/reply/followup-runner.tscalledreplyOperation.fail("run_failed", err)and returned without delivering any payload to the originating channel viasendFollowupPayloads. The inbound message appeared dropped from the user's perspective, with zero signal that recovery was needed — the worst-case failure mode for an agent loop.This change classifies the failure message and, on a positive match, sends a single short notice payload so the originating channel receives:
Approach
Following the direction sketched in the issue's "Suggested direction" section:
isFollowupQuotaBillingFailure(message)exported fromfollowup-runner.ts. Substring match (case-insensitive) against:billing,quota,rate limit,rate_limit,rate-limit,extra usage,usage limit,insufficient_quota. Empty input returnsfalse.replyOperation.fail()and the existing error log, if the classifier matches andrun.sourceReplyDeliveryMode !== "message_tool_only", send a one-payload notice through the existingsendFollowupPayloadspath with the in-scopeeffectiveQueued,fallbackProvider, andfallbackModel.defaultRuntime.error?.(...)so they cannot mask the original error or throw out of the catch.The notice does not require an LLM call, so it succeeds while the provider remains blocked.
Why this shape
surface_errorin the model-fallback layer instead. Keeping the change local to the followup catch block matches the existingsourceReplyDeliveryModesuppression pattern already in this file (line 458) and is minimally invasive.message_tool_onlyparity. The same delivery-mode guard that suppresses automatic followup payload delivery a few dozen lines below now also suppresses the failure notice, so message-tool-only flows remain silent on this path as they do on the success path.sendFollowupPayloadsin a try/catch with a structured error log ensures a misrouted/unavailable origin can't turn the catch block into a thrown exception that re-bubbles pastreplyOperation.fail().Tests
Adds 6 regression tests to
src/auto-reply/reply/followup-runner.test.tsin a newcreateFollowupRunner quota/billing failure notice (#80700)suite:isFollowupQuotaBillingFailurecovering the documented patterns plus the empty/unrelated negatives.insufficient_quotaerror.429 ... rate limiterror.socket hang uperror (negative case).sourceReplyDeliveryMode === "message_tool_only".Full file suite: 36/36 passing (30 pre-existing + 6 new). Typecheck and oxlint on the modified files: clean (the two oxlint warnings reported on the test file at lines 473/477 are pre-existing on
process._getActiveHandles/_getActiveRequestsand were untouched by this PR).Out of scope / open questions for maintainers
surface_errordecision should also fan out to followup. Left untouched.Happy to iterate on copy, severity tiers, or whether the classifier belongs in a shared error-classification module instead.