@@ -23,7 +23,10 @@ import {
2323 resolveRunAfterAutoFallbackPrimaryProbeRecheck ,
2424} from "./agent-runner-execution.js" ;
2525import { HEARTBEAT_EXTERNAL_RUN_FAILURE_TEXT } from "./agent-runner-failure-copy.js" ;
26- import { PROVIDER_CONVERSATION_STATE_ERROR_USER_MESSAGE } from "./provider-request-error-classifier.js" ;
26+ import {
27+ PROVIDER_CONVERSATION_STATE_ERROR_USER_MESSAGE ,
28+ PROVIDER_RATE_LIMIT_OR_QUOTA_ERROR_USER_MESSAGE ,
29+ } from "./provider-request-error-classifier.js" ;
2730import type { FollowupRun } from "./queue.js" ;
2831import type { ReplyOperation } from "./reply-run-registry.js" ;
2932import type { TypingSignaler } from "./typing-mode.js" ;
@@ -98,31 +101,36 @@ vi.mock("../../agents/bootstrap-budget.js", () => ({
98101 resolveBootstrapWarningSignaturesSeen : ( ) => [ ] ,
99102} ) ) ;
100103
101- vi . mock ( "../../agents/embedded-agent-helpers.js" , ( ) => ( {
102- BILLING_ERROR_USER_MESSAGE : "billing" ,
103- formatRateLimitOrOverloadedErrorCopy : ( message : string ) => {
104- if ( / m o d e l \s + (?: i s \s + ) ? a t c a p a c i t y / i. test ( message ) ) {
105- return "⚠️ Selected model is at capacity. Try a different model, or wait and retry." ;
106- }
107- if ( / r a t e .l i m i t | t o o m a n y r e q u e s t s | 4 2 9 / i. test ( message ) ) {
108- return "⚠️ API rate limit reached. Please try again later." ;
109- }
110- if ( / o v e r l o a d e d / i. test ( message ) ) {
111- return "The AI service is temporarily overloaded. Please try again in a moment." ;
112- }
113- return undefined ;
114- } ,
115- isCompactionFailureError : ( message ?: string ) => state . isCompactionFailureErrorMock ( message ) ,
116- isContextOverflowError : ( message ?: string ) => state . isContextOverflowErrorMock ( message ) ,
117- isBillingErrorMessage : ( ) => false ,
118- isLikelyContextOverflowError : ( message ?: string ) =>
119- state . isLikelyContextOverflowErrorMock ( message ) ,
120- isOverloadedErrorMessage : ( message : string ) => / o v e r l o a d e d | c a p a c i t y / i. test ( message ) ,
121- isRateLimitErrorMessage : ( message : string ) =>
122- / r a t e .l i m i t | t o o m a n y r e q u e s t s | 4 2 9 | u s a g e l i m i t / i. test ( message ) ,
123- isTransientHttpError : ( ) => false ,
124- sanitizeUserFacingText : ( text ?: string ) => text ?? "" ,
125- } ) ) ;
104+ vi . mock ( "../../agents/embedded-agent-helpers.js" , async ( ) => {
105+ const actual = await vi . importActual < typeof import ( "../../agents/embedded-agent-helpers.js" ) > (
106+ "../../agents/embedded-agent-helpers.js" ,
107+ ) ;
108+ return {
109+ BILLING_ERROR_USER_MESSAGE : "billing" ,
110+ formatRateLimitOrOverloadedErrorCopy : ( message : string ) => {
111+ if ( / m o d e l \s + (?: i s \s + ) ? a t c a p a c i t y / i. test ( message ) ) {
112+ return "⚠️ Selected model is at capacity. Try a different model, or wait and retry." ;
113+ }
114+ if ( / r a t e .l i m i t | t o o m a n y r e q u e s t s | 4 2 9 / i. test ( message ) ) {
115+ return "⚠️ API rate limit reached. Please try again later." ;
116+ }
117+ if ( / o v e r l o a d e d / i. test ( message ) ) {
118+ return "The AI service is temporarily overloaded. Please try again in a moment." ;
119+ }
120+ return undefined ;
121+ } ,
122+ isCompactionFailureError : ( message ?: string ) => state . isCompactionFailureErrorMock ( message ) ,
123+ isContextOverflowError : ( message ?: string ) => state . isContextOverflowErrorMock ( message ) ,
124+ isBillingErrorMessage : actual . isBillingErrorMessage ,
125+ isLikelyContextOverflowError : ( message ?: string ) =>
126+ state . isLikelyContextOverflowErrorMock ( message ) ,
127+ isOverloadedErrorMessage : ( message : string ) => / o v e r l o a d e d | c a p a c i t y / i. test ( message ) ,
128+ isRateLimitErrorMessage : ( message : string ) =>
129+ / r a t e .l i m i t | t o o m a n y r e q u e s t s | 4 2 9 | u s a g e l i m i t / i. test ( message ) ,
130+ isTransientHttpError : ( ) => false ,
131+ sanitizeUserFacingText : ( text ?: string ) => text ?? "" ,
132+ } ;
133+ } ) ;
126134
127135vi . mock ( "../../config/sessions.js" , ( ) => ( {
128136 resolveGroupSessionKey : vi . fn ( ( ) => null ) ,
@@ -5420,6 +5428,58 @@ describe("runAgentTurnWithFallback", () => {
54205428 }
54215429 } ) ;
54225430
5431+ it ( "surfaces provider quota guidance for generic HTTP 429 failures before reply" , async ( ) => {
5432+ const error = new Error (
5433+ "Something went wrong while processing your request. Please try again." ,
5434+ ) ;
5435+ Object . assign ( error , { status : 429 } ) ;
5436+ state . runEmbeddedAgentMock . mockRejectedValueOnce ( error ) ;
5437+
5438+ const runAgentTurnWithFallback = await getRunAgentTurnWithFallback ( ) ;
5439+ const result = await runAgentTurnWithFallback (
5440+ createMinimalRunAgentTurnParams ( {
5441+ sessionCtx : {
5442+ Provider : "discord" ,
5443+ Surface : "discord" ,
5444+ ChatType : "direct" ,
5445+ MessageSid : "msg" ,
5446+ } as unknown as TemplateContext ,
5447+ } ) ,
5448+ ) ;
5449+
5450+ expect ( result . kind ) . toBe ( "final" ) ;
5451+ if ( result . kind === "final" ) {
5452+ expect ( result . payload . text ) . toBe ( PROVIDER_RATE_LIMIT_OR_QUOTA_ERROR_USER_MESSAGE ) ;
5453+ expect ( result . payload . text ) . not . toBe ( GENERIC_RUN_FAILURE_TEXT ) ;
5454+ }
5455+ } ) ;
5456+
5457+ it ( "surfaces billing guidance for Volcengine Coding Plan subscription failures before reply" , async ( ) => {
5458+ state . runEmbeddedAgentMock . mockRejectedValueOnce (
5459+ new Error (
5460+ 'HTTP 400 Bad Request: {"error":{"code":"InvalidSubscription","message":"Your account does not have a valid CodingPlan subscription, or your subscription has expired."}}' ,
5461+ ) ,
5462+ ) ;
5463+
5464+ const runAgentTurnWithFallback = await getRunAgentTurnWithFallback ( ) ;
5465+ const result = await runAgentTurnWithFallback (
5466+ createMinimalRunAgentTurnParams ( {
5467+ sessionCtx : {
5468+ Provider : "discord" ,
5469+ Surface : "discord" ,
5470+ ChatType : "direct" ,
5471+ MessageSid : "msg" ,
5472+ } as unknown as TemplateContext ,
5473+ } ) ,
5474+ ) ;
5475+
5476+ expect ( result . kind ) . toBe ( "final" ) ;
5477+ if ( result . kind === "final" ) {
5478+ expect ( result . payload . text ) . toBe ( "billing" ) ;
5479+ expect ( result . payload . text ) . not . toBe ( GENERIC_RUN_FAILURE_TEXT ) ;
5480+ }
5481+ } ) ;
5482+
54235483 it ( "formats raw Codex API payloads before forwarding verbose external errors" , async ( ) => {
54245484 state . runEmbeddedAgentMock . mockRejectedValueOnce (
54255485 new Error (
0 commit comments