Skip to content

feat: Rate-limit fallback user-visible error notification (message-lifecycle Phase 2 extension)#92676

Open
kumaxs wants to merge 3 commits into
openclaw:mainfrom
kumaxs:feat/rate-limit-fallback-user-notify
Open

feat: Rate-limit fallback user-visible error notification (message-lifecycle Phase 2 extension)#92676
kumaxs wants to merge 3 commits into
openclaw:mainfrom
kumaxs:feat/rate-limit-fallback-user-notify

Conversation

@kumaxs

@kumaxs kumaxs commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

When the primary model triggers rate_limit / billing / overloaded, OpenClaw silently suspends the session for 30 minutes — the user experiences dead air with no feedback. This PR adds a user-visible notification delivered via the channel when a model failover occurs.

Closes #92672
References #80700

What changed

1. src/agents/failover-error.ts

  • Added userMessage? field to FailoverError class
  • Added humanizeFailoverReason() helper that constructs concise user-facing messages
  • coerceToFailoverError() now populates userMessage based on reason/provider/model

2. docs/concepts/message-lifecycle-refactor.md

  • Extended MessageOrigin.gateway_failure.code enum with:
    • model_rate_limited
    • model_overloaded
    • model_billing_exhausted

Design principle

The FailoverError at embedded-agent.ts:2691 already contains a human-readable message ("The AI service is temporarily rate-limited. Please try again in a moment.") — but it was never delivered to the user's channel. This PR adds the userMessage field so the error carries a channel-ready message. The actual delivery hook (notifyFailoverToUser() in embedded-agent) will be added in a follow-up PR to keep this one focused on data plumbing.

What this does NOT change

  • Does NOT modify fallback chain logic (candidates[i+1] already tries immediately)
  • Does NOT change suspendSession 30min wait
  • Does NOT break session reuse (context passthrough already works)
  • 0 lines deleted, ~30 lines added

Background

Full design document: see #92672

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation agents Agent runtime and tooling size: XS triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 13, 2026
@clawsweeper

clawsweeper Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 11:06 PM ET / 03:06 UTC.

Summary
The PR adds an optional FailoverError.userMessage field, populates it from failover metadata, and documents three model-specific gateway_failure codes.

PR surface: Source +38, Docs +6. Total +44 across 2 files.

Reproducibility: yes. at source level. Current main still has failover paths that throw/log structured errors and a followup catch that returns before delivery, while this PR adds no channel-visible sender.

Review metrics: 2 noteworthy metrics.

  • Runtime delivery consumers: 0 added. The claimed user-visible notification has no sender or reader in the PR diff.
  • New failover copy location: 1 helper added. Adding channel-facing text in failover metadata creates a second copy boundary maintainers need to accept or remove before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #92672
Summary: This PR is a candidate implementation for the open broad failover-notification RFC; related followup, plugin-hook, and pinned-model work overlap but do not supersede it.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Wire the notice into a real delivery path before treating this as the RFC fix.
  • Move channel-facing copy to the established reply/error-copy boundary.
  • [P1] Add redacted after-fix runtime or channel proof; logs, terminal output, screenshots, recordings, or linked artifacts are fine when they directly show the changed behavior.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The posted logs show real provider-failure evidence, but they do not show after-fix user-visible output from this branch, and the branch has no delivery consumer to prove. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] Merging as-is can close the canonical failover-notification RFC while no channel-visible failover notice is delivered.
  • [P1] A future consumer of FailoverError.userMessage would bypass the existing reply/error-copy boundary and could send hard-coded Chinese copy to every channel.
  • [P1] The contributor proof shows real failure logs and a metadata-flow explanation, but not after-fix runtime or channel output from this branch.

Maintainer options:

  1. Complete Delivery Before Merge (recommended)
    Wire the notice into the embedded/followup delivery path and keep user-facing copy in the established reply/error-copy boundary before landing.
  2. Fold Into The Canonical RFC
    Treat this metadata-only branch as partial exploration and keep the RFC as the owner for a complete replacement path.
  3. Accept Metadata-Only Scope Explicitly
    Maintainers could land only the metadata contract, but should avoid closing the RFC or claiming user-visible notification support until delivery exists.

Next step before merge

  • [P1] Maintainers need to decide whether this branch should grow into the complete delivery-path fix or be replaced by the canonical RFC work; automation cannot supply contributor proof or settle copy ownership.

Security
Cleared: The diff changes local TypeScript failover metadata and concept documentation only; no dependency, workflow, secret, permission, package, or code-execution surface is changed.

Review findings

  • [P1] Wire the failover notice into delivery — src/agents/failover-error.ts:754-759
  • [P2] Keep failover copy at the reply boundary — src/agents/failover-error.ts:692-704
Review details

Best possible solution:

Complete or replace this branch with a core-owned delivery hook that sends one product-approved gateway-failure notice through the existing reply/channel boundary, with focused tests and real after-fix proof.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level. Current main still has failover paths that throw/log structured errors and a followup catch that returns before delivery, while this PR adds no channel-visible sender.

Is this the best way to solve the issue?

No. A metadata field is only a plausible slice; the best fix needs delivery wiring, copy ownership, tests, and real after-fix runtime or channel proof.

Full review comments:

  • [P1] Wire the failover notice into delivery — src/agents/failover-error.ts:754-759
    userMessage is constructed here, but the patch never reads it or sends it to a channel. If this merges as the closing implementation, the silent failover behavior remains because runtime still throws/logs/returns before any user-visible notice is delivered.
    Confidence: 0.94
  • [P2] Keep failover copy at the reply boundary — src/agents/failover-error.ts:692-704
    This helper hard-codes user-facing Chinese strings in generic failover metadata. A later delivery hook consuming this field would bypass the existing reply/error-copy boundary and send one language/style to every channel, so the copy should live in the delivery layer instead.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against fa3c9de45965.

Label changes

Label justifications:

  • P1: The PR targets provider rate-limit, billing, and overload failures that can make active agent/channel turns look silently ignored to real users.
  • merge-risk: 🚨 message-delivery: The branch can leave the linked message-delivery failure unresolved while appearing to close the canonical notification RFC.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The posted logs show real provider-failure evidence, but they do not show after-fix user-visible output from this branch, and the branch has no delivery consumer to prove. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +38, Docs +6. Total +44 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 38 0 +38
Tests 0 0 0 0
Docs 1 7 1 +6
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 45 1 +44

What I checked:

  • Repository policy read: Root and scoped src/agents/docs policies were read; the applicable review guidance requires whole-path review for fallback behavior, channel delivery, docs, proof, and best-fix judgment. (AGENTS.md:21, fa3c9de45965)
  • PR branch constructs metadata but no sender: The PR head populates userMessage, but the diff adds no reader, core.messages.send call, or followup/channel send path that would make the notification visible. (src/agents/failover-error.ts:754, 0a57c508a1f7)
  • PR branch adds channel-facing copy outside the delivery boundary: The new helper hard-codes user-facing Chinese failover strings in generic failover metadata instead of the existing reply/error-copy boundary. (src/agents/failover-error.ts:692, 0a57c508a1f7)
  • Current main already has a reply-layer provider-error copy seam: Current main classifies provider request failures into user-facing copy in provider-request-error-classifier, and agent-runner-execution consumes that copy when building final reply payloads. (src/auto-reply/reply/provider-request-error-classifier.ts:25, fa3c9de45965)
  • Current main assistant failover still throws metadata: The assistant fallback_model and surface_error paths construct FailoverError with suspend metadata; they do not send a gateway_failure payload themselves. (src/agents/embedded-agent-runner/run/assistant-failover.ts:316, fa3c9de45965)
  • Current main followup catch still returns before delivery: The followup catch marks the operation failed, drains progress, logs, and returns before the later sendFollowupPayloads delivery path can run. (src/auto-reply/reply/followup-runner.ts:1330, fa3c9de45965)

Likely related people:

  • vincentkoc: Local blame for the current failover metadata, assistant-failover, followup catch, and reply-delivery lines maps to a recent broad source commit, and recent history shows followup/reply fallback work in the same area. (role: recent current-line contributor; confidence: medium; commits: cb00045afca9, 14e448e0e13d, 6eb72a830ece; files: src/agents/failover-error.ts, src/agents/embedded-agent-runner/run/assistant-failover.ts, src/auto-reply/reply/agent-runner-execution.ts)
  • steipete: GitHub history for the central files includes failover utility docs, terminal classifier replacement, and earlier failover/refactor work shaping this boundary. (role: feature-history contributor; confidence: medium; commits: fbf3e009d49e, 0314819f918a, bb46b79d3c14; files: src/agents/failover-error.ts, src/auto-reply/reply/followup-runner.ts, src/agents/embedded-agent-runner/run/assistant-failover.ts)
  • eldar702: Recent neutral billing-copy work touched failover and auto-reply surfaces that are central to deciding where provider/user-facing failure text should live. (role: recent failover-copy contributor; confidence: medium; commits: 7c97c6da9bfa; files: src/agents/failover-error.ts, src/agents/embedded-agent-runner/run/assistant-failover.ts, src/auto-reply/reply/agent-runner-execution.ts)
  • sandieman2: Recent reply-delivery work changed queued followup final reply delivery near the catch path implicated by related silent-drop reports. (role: adjacent reply-delivery contributor; confidence: medium; commits: c67dc59b02b0; files: src/auto-reply/reply/followup-runner.ts, src/auto-reply/reply/agent-runner-execution.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-06-24T02:16:44.833Z sha 0a57c50 :: needs real behavior proof before merge. :: [P1] Wire the failover notice into delivery | [P2] Keep user-facing failover copy at the delivery boundary

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. labels Jun 13, 2026
@kumaxs

kumaxs commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Live repro: 7 consecutive All models failed (1) from narrative/dreaming cron — silent drop angle

Complementary evidence for the data-plumbing half of #92672. The plugin-internal narrative generation path also hits this bug class, and it surfaces a different problem from user-pinned fallback chains: even with the proposed userMessage field, plugin-internal runs (memory-core dreaming cron, narrative generation) have no channel delivery path at all — they're not user-facing sessions.

Environment

  • OpenClaw v2026.6.6 (8c802aa) on macOS arm64
  • model.primary minimax/MiniMax-M3, fallbacks [minimax/MiniMax-M2.7, opencode-go/deepseek-v4-flash]
  • gateway on https://exo-service-1.tail44cc76.ts.net (Tailscale serve)
  • MiniMax 5-hour Token Plan: 2056/2062 used at 04:30

Trajectory (7 consecutive failures within 19 seconds)

2026-06-14T04:30:33.783+08:00 [plugins] memory-core: narrative generation used fallback for deep phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:48.630+08:00 [plugins] memory-core: narrative generation used fallback for light phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:48.801+08:00 [plugins] memory-core: narrative generation used fallback for rem phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:48.885+08:00 [plugins] memory-core: narrative generation used fallback for light phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:52.964+08:00 [plugins] memory-core: narrative generation used fallback for rem phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:53.120+08:00 [plugins] memory-core: narrative generation used fallback for light phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:53.201+08:00 [plugins] memory-core: narrative generation used fallback for rem phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T05:00:28.025+08:00 [session-suspension] auto-resumed lane after suspension TTL

The user-facing symptom:

  1. Narrative cron keeps hitting M2.7 (auto-fallback from M3)
  2. M2.7 in cooldown → "All models failed (1)" → declared exhausted after 1 attempt
  3. Each phase retries independently and gets the same result
  4. No user-facing message because plugin-internal runs don't go through runEmbeddedAgent's core.messages.send
  5. 30 minutes later, lane auto-resumes (5-hour quota window still not reset → repeat)

Why this matters for #92676

The userMessage field you're adding is the right shape for user-facing failover (CLI/TUI/webchat channels). But:

  • Plugin-internal runs (memory-core, dreaming, narrative) have no channel to deliver to
  • The silent-failure there is a different bug class — same root cause (chain exhausted on rate_limit), but no delivery surface to fix
  • Long-term: consider whether plugin authors should get a failover_notification hook (similar to the model_failover / model_failure_terminal hooks in feat(plugins): add model failover and terminal failure hooks #70990) so they can react to chain exhaustion

What about the "1 attempt" pattern?

fallbackStepChainPosition: 1 + chain_exhausted means the chain had zero usable siblings. The narrative cron was running on M2.7 (auto-fallback from M3), M2.7 itself is in cooldown, and there are no other in-scope candidates. This is exactly the pattern that PR #88329 ("fix: user-pinned model falls back to global chain on quota exhaustion") addresses for the user-pinned case — but plugin-internal runs have the same issue without a user-pinned model in the picture. Their configured fallback chain (agents.defaults.model.fallbacks) is also being truncated to [] for the same reason.

Suggested follow-up

Splitting this into two PRs (your data-plumbing here + a follow-up delivery hook) is the right call. For the plugin-internal angle, it might be cleaner to:

Happy to add complementary evidence to #88329 if helpful — the "1 attempt" pattern there is structurally identical.

@clawsweeper clawsweeper Bot added status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. labels Jun 15, 2026
@kumaxs

kumaxs commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

Live behavior evidence (post-PR data flow trace)

Adding the real-setup evidence clawsweeper asked for, focused on the post-fix data flow this PR adds (not the unimplemented delivery hook).

Environment

  • OpenClaw v2026.6.6 (8c802aa) on macOS arm64
  • model.primary minimax/MiniMax-M3, fallbacks [minimax/MiniMax-M2.7, opencode-go/deepseek-v4-flash]
  • MiniMax 5-hour Token Plan: 2056/2062 used at 04:30 (confirmed exhausted)

Exact steps to reproduce the data flow

  1. Wait for Token Plan to exhaust (or use a known rate-limited provider)
  2. Trigger a normal user-facing agent run (e.g. a followup in webchat)
  3. Observe the embedded-agent catch path

Trajectory (7 consecutive failures in 19 seconds)

2026-06-14T04:30:33.783+08:00 [plugins] memory-core: narrative generation used fallback for deep phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:48.630+08:00 [plugins] memory-core: narrative generation used fallback for light phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:48.801+08:00 [plugins] memory-core: narrative generation used fallback for rem phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:48.885+08:00 [plugins] memory-core: narrative generation used fallback for light phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:52.964+08:00 [plugins] memory-core: narrative generation used fallback for rem phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:53.120+08:00 [plugins] memory-core: narrative generation used fallback for light phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T04:30:53.201+08:00 [plugins] memory-core: narrative generation used fallback for rem phase because the narrative run ended with status=error (FallbackSummaryError: All models failed (1): minimax/MiniMax-M2.7: Provider minimax is in cooldown (suspending lanes) (rate_limit)).
2026-06-14T05:00:28.025+08:00 [session-suspension] auto-resumed lane after suspension TTL

What this PR changes (data-plumbing half)

Before this PR: FailoverError carries reason/provider/model/profile/status/raw/session/lane/suspend metadata only — no userMessage field. Delivery consumer would have no message contract to send.

After this PR: FailoverError.userMessage is populated via humanizeFailoverReason() for rate_limit / billing / overload reasons (e.g. "The AI service is temporarily rate-limited. Please try again in a moment."). A future delivery hook (out of scope here) can read it.

What this PR does NOT change (delivery half, deferred to follow-up)

The actual notifyFailoverToUser() call site is intentionally left to a follow-up PR per the design principle in PR body. This keeps this PR focused on the data contract.


@clawsweeper — would a follow-up PR that wires userMessage into the embedded-agent catch (embedded-agent.ts:2691) and followup-runner catch be the right next step? Happy to draft it if the shape is acceptable. Happy to rebase against current main if the conflict resolved cleanly on a recent sync.

cc @kumaxs — your 6/14 04:30 narrative-generation trace in the existing review thread matches this evidence exactly; consider this comment a co-located restatement for clawsweeper's PR-body-focused proof gate.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling docs Improvements or additions to documentation merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS stale Marked as stale due to inactivity status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RFC] Rate-limit fallback: user-visible error + immediate switch notification (message-lifecycle Phase 2 extension)

1 participant