@@ -480,6 +480,8 @@ describe("CodexAdapterV2 fork boundary", () => {
480480} ) ;
481481
482482const DEFAULT_CODEX_SETTINGS = Schema . decodeSync ( CodexSettings ) ( { } ) ;
483+ const CHATGPT_SHARING_LIMIT_MESSAGE =
484+ "stream disconnected before completion: The ChatGPT user has reached their Subscription Sharing usage limit. Ask the user to try again after their usage limit resets or use an API key instead." ;
483485const CODEX_TEST_MODEL_SELECTION = {
484486 instanceId : CodexAdapterV2 . CODEX_DEFAULT_INSTANCE_ID ,
485487 model : "gpt-5.4" ,
@@ -1235,6 +1237,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
12351237 onRequest : ( method : string , params : unknown ) => Effect . Effect < void > = ( ) => Effect . void ,
12361238 readChildMetadata ?: Parameters < typeof withCodexReplayChildMetadata > [ 2 ] ,
12371239 beforeEmitInbound ?: CodexReplay . CodexAppServerReplayDriver [ "beforeEmitInbound" ] ,
1240+ onUsageLimits ?: CodexAdapterV2 . CodexAdapterV2Options [ "onUsageLimits" ] ,
12381241 ) =>
12391242 Effect . gen ( function * ( ) {
12401243 const fileSystem = yield * FileSystem . FileSystem ;
@@ -1292,6 +1295,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
12921295 settings : DEFAULT_CODEX_SETTINGS ,
12931296 environment : { } ,
12941297 clientFactory,
1298+ ...( onUsageLimits ? { onUsageLimits } : { } ) ,
12951299 digest : yield * Digest . Digest ,
12961300 fileSystem,
12971301 idAllocator,
@@ -5612,14 +5616,58 @@ describe("CodexAdapterV2 post-settle continuation", () => {
56125616 code : "other" ,
56135617 details : "subscription_sharing_workplace_individual_usage_limit_exceeded" ,
56145618 notification : true ,
5619+ usedBefore : 100 ,
5620+ expectedClass : "usage_limit" ,
5621+ expectedCode : "subscription_sharing_workplace_individual_usage_limit_exceeded" ,
5622+ expectedMessage : "workspace admin" ,
5623+ } ,
5624+ // regression: #25333 ChatGPT sharing limit prose classifies as a usage limit
5625+ {
5626+ name : "chatgpt-sharing-prose-retry" ,
5627+ code : "responseStreamDisconnected" ,
5628+ errorInfo : { responseStreamDisconnected : { httpStatusCode : null } } ,
5629+ message : CHATGPT_SHARING_LIMIT_MESSAGE ,
5630+ notification : true ,
5631+ willRetry : true ,
5632+ usedBefore : 100 ,
5633+ expectedClass : "usage_limit" ,
5634+ expectedCode : "subscription_sharing_usage_limit_exceeded" ,
5635+ expectedMessage : "Usage settings" ,
5636+ } ,
5637+ {
5638+ name : "chatgpt-sharing-prose" ,
5639+ code : "responseStreamDisconnected" ,
5640+ errorInfo : { responseStreamDisconnected : { httpStatusCode : null } } ,
5641+ message : CHATGPT_SHARING_LIMIT_MESSAGE ,
5642+ notification : false ,
5643+ resetAfter : true ,
5644+ expectedClass : "usage_limit" ,
5645+ expectedCode : "subscription_sharing_usage_limit_exceeded" ,
5646+ expectedMessage : "Usage settings" ,
5647+ } ,
5648+ {
5649+ name : "sharing-code-mention" ,
5650+ code : "responseStreamDisconnected" ,
5651+ errorInfo : { responseStreamDisconnected : { httpStatusCode : null } } ,
5652+ message : "Transport unavailable; this is not subscription_sharing_usage_limit_exceeded." ,
5653+ notification : false ,
56155654 expectedClass : "provider_error" ,
56165655 } ,
5656+ {
5657+ name : "sharing-code-mention-retry" ,
5658+ code : "responseStreamDisconnected" ,
5659+ errorInfo : { responseStreamDisconnected : { httpStatusCode : null } } ,
5660+ message : "Transport unavailable; this is not subscription_sharing_usage_limit_exceeded." ,
5661+ notification : true ,
5662+ willRetry : true ,
5663+ expectedClass : "transport_error" ,
5664+ } ,
56175665 ] as const ) ( "classifies Codex terminal failures from $name evidence" , ( scenario ) =>
56185666 Effect . scoped (
56195667 Effect . gen ( function * ( ) {
56205668 const nativeThreadId = `native-limit-${ scenario . name } ` ;
56215669 const nativeTurnId = `turn-limit-${ scenario . name } ` ;
5622- const message = "Provider stopped this request." ;
5670+ const message = "message" in scenario ? scenario . message : " Provider stopped this request.";
56235671 const errorInfo = "errorInfo" in scenario ? scenario . errorInfo : scenario . code ;
56245672 const resetAt = "2033-05-19T07:20:00.000Z" ;
56255673 const snapshot = {
@@ -5639,6 +5687,26 @@ describe("CodexAdapterV2 post-settle continuation", () => {
56395687 scenario : `codex-limit-${ scenario . name } ` ,
56405688 entries : [
56415689 ...codexReplayPreamble ( { nativeThreadId, nativeTurnId, prompt : "Continue." } ) ,
5690+ ...( "usedBefore" in scenario
5691+ ? [
5692+ {
5693+ ...snapshot ,
5694+ frame : {
5695+ method : "account/rateLimits/updated" ,
5696+ params : {
5697+ rateLimits : {
5698+ limitId : "codex" ,
5699+ primary : {
5700+ usedPercent : scenario . usedBefore ,
5701+ resetsAt : 2000100000 ,
5702+ windowDurationMins : 300 ,
5703+ } ,
5704+ } ,
5705+ } ,
5706+ } ,
5707+ } ,
5708+ ]
5709+ : [ ] ) ,
56425710 ...( scenario . name === "deferred-reset" ? [ snapshot ] : [ ] ) ,
56435711 ...( scenario . name === "deferred-reset"
56445712 ? [
@@ -5686,7 +5754,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
56865754 params : {
56875755 threadId : nativeThreadId ,
56885756 turnId : nativeTurnId ,
5689- willRetry : scenario . name === "retry" ,
5757+ willRetry : scenario . name === "retry" || "willRetry" in scenario ,
56905758 error : {
56915759 message,
56925760 codexErrorInfo : errorInfo ,
@@ -5723,7 +5791,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
57235791 } ,
57245792 } ,
57255793 } ,
5726- ...( scenario . name === "late-reset" ? [ snapshot ] : [ ] ) ,
5794+ ...( scenario . name === "late-reset" || "resetAfter" in scenario ? [ snapshot ] : [ ] ) ,
57275795 ...( scenario . name === "deferred-reset"
57285796 ? [
57295797 {
@@ -5761,12 +5829,19 @@ describe("CodexAdapterV2 post-settle continuation", () => {
57615829 ] ,
57625830 } ) ;
57635831 const resetReceipt = yield * Deferred . make < void > ( ) ;
5764- const harness = yield * makeCodexReplayHarness ( transcript , ( event ) =>
5765- event . type === "turn_item.updated" &&
5766- event . turnItem . type === "error" &&
5767- event . turnItem . failure . resetAt === resetAt
5768- ? Deferred . succeed ( resetReceipt , undefined )
5769- : Effect . void ,
5832+ const snapshotReceipt = yield * Deferred . make < void > ( ) ;
5833+ const harness = yield * makeCodexReplayHarness (
5834+ transcript ,
5835+ ( event ) =>
5836+ event . type === "turn_item.updated" &&
5837+ event . turnItem . type === "error" &&
5838+ event . turnItem . failure . resetAt === resetAt
5839+ ? Deferred . succeed ( resetReceipt , undefined )
5840+ : Effect . void ,
5841+ undefined ,
5842+ undefined ,
5843+ undefined ,
5844+ ( ) => Deferred . succeed ( snapshotReceipt , undefined ) ,
57705845 ) ;
57715846 yield * harness . runtime . startTurn (
57725847 makeCodexTestTurnInput ( {
@@ -5786,6 +5861,17 @@ describe("CodexAdapterV2 post-settle continuation", () => {
57865861 if ( scenario . name === "deferred-reset" ) assert . equal ( terminal . failure . resetAt , resetAt ) ;
57875862 if ( scenario . name === "matching-details" )
57885863 assert . equal ( terminal . failure . message , "Detailed provider allowance explanation." ) ;
5864+ if ( "resetAfter" in scenario ) {
5865+ yield * Deferred . await ( snapshotReceipt ) ;
5866+ assert . isFalse (
5867+ harness . events . some (
5868+ ( event ) =>
5869+ event . type === "turn_item.updated" &&
5870+ event . turnItem . type === "error" &&
5871+ event . turnItem . failure . resetAt != null ,
5872+ ) ,
5873+ ) ;
5874+ }
57895875 if ( scenario . name === "late-reset" ) {
57905876 yield * Deferred . await ( resetReceipt ) ;
57915877 const item = harness . events . find (
@@ -5796,10 +5882,12 @@ describe("CodexAdapterV2 post-settle continuation", () => {
57965882 ) ;
57975883 assert . isDefined ( item ) ;
57985884 }
5799- if ( scenario . name === "retry" ) assert . equal ( terminal . retry ?. attempt , 1 ) ;
5800- if ( scenario . name === "chatgpt-workspace-limit" ) {
5801- assert . equal ( terminal . failure . code , scenario . details ) ;
5802- assert . include ( terminal . failure . message , "workspace admin" ) ;
5885+ if ( scenario . name === "retry" || "willRetry" in scenario )
5886+ assert . equal ( terminal . retry ?. attempt , 1 ) ;
5887+ if ( "expectedCode" in scenario && "expectedMessage" in scenario ) {
5888+ assert . equal ( terminal . failure . code , scenario . expectedCode ) ;
5889+ assert . include ( terminal . failure . message , scenario . expectedMessage ) ;
5890+ assert . equal ( terminal . failure . resetAt , null ) ;
58035891 }
58045892 } ) . pipe (
58055893 Effect . provide (
0 commit comments