@@ -4968,4 +4968,158 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
49684968 bestEffortDeliver : true ,
49694969 } ) ;
49704970 } ) ;
4971+
4972+ it ( "does not retry session-file-changed failures with send evidence" , async ( ) => {
4973+ const sendErr = new OutboundDeliveryError ( "outbound delivery failed" , {
4974+ cause : new Error ( "outbound delivery failed" ) ,
4975+ results : [ { channel : "telegram" , messageId : "msg-1" } ] ,
4976+ } ) ;
4977+ const callGateway : typeof runtimeCallGateway = vi . fn ( async ( ) => {
4978+ throw new Error ( "session file changed while embedded prompt lock was released" , {
4979+ cause : sendErr ,
4980+ } ) ;
4981+ } ) ;
4982+ const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock ( [ "no_active_run" ] ) ;
4983+ const result = await deliverSlackChannelAnnouncement ( {
4984+ callGateway,
4985+ queueEmbeddedAgentMessageWithOutcome,
4986+ sessionId : "requester-session-lock-race-evidence" ,
4987+ isActive : true ,
4988+ expectsCompletionMessage : true ,
4989+ directIdempotencyKey : "announce-permanent-lock-error-evidence" ,
4990+ } ) ;
4991+
4992+ expect ( result . delivered ) . toBe ( false ) ;
4993+ expect ( result . path ) . toBe ( "direct" ) ;
4994+ expect ( result . terminal ) . toBe ( true ) ;
4995+ expect ( result . phases ?. map ( ( phase ) => phase . phase ) ) . toEqual ( [ "direct-primary" ] ) ;
4996+ expect ( callGateway ) . toHaveBeenCalledTimes ( 1 ) ;
4997+ expect ( queueEmbeddedAgentMessageWithOutcome ) . toHaveBeenCalledTimes ( 1 ) ;
4998+ } ) ;
4999+
5000+ it ( "does not fallback-steer after wrapped prompt-lock takeover with send evidence" , async ( ) => {
5001+ const takeoverErr = Object . assign (
5002+ new Error ( "session file changed while embedded prompt lock was released: /tmp/session.jsonl" ) ,
5003+ { name : "EmbeddedAttemptSessionTakeoverError" } ,
5004+ ) ;
5005+
5006+ const promptErr = Object . assign ( new Error ( "some model error" ) , { visibleReplySent : true } ) ;
5007+ const wrapperErr = Object . assign ( new Error ( "some model error" , { cause : takeoverErr } ) , {
5008+ name : "EmbeddedAttemptSessionTakeoverError" ,
5009+ cleanupError : takeoverErr ,
5010+ promptError : promptErr ,
5011+ } ) ;
5012+
5013+ const callGateway : typeof runtimeCallGateway = vi . fn ( async ( ) => {
5014+ throw wrapperErr ;
5015+ } ) ;
5016+ const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock ( [ "no_active_run" ] ) ;
5017+ const result = await deliverSlackChannelAnnouncement ( {
5018+ callGateway,
5019+ queueEmbeddedAgentMessageWithOutcome,
5020+ sessionId : "requester-session-lock-race-wrapped-evidence" ,
5021+ isActive : true ,
5022+ expectsCompletionMessage : true ,
5023+ directIdempotencyKey : "announce-permanent-wrapped-lock-error-evidence" ,
5024+ } ) ;
5025+
5026+ expect ( result . delivered ) . toBe ( false ) ;
5027+ expect ( result . path ) . toBe ( "direct" ) ;
5028+ expect ( result . error ) . toBe ( "some model error" ) ;
5029+ expect ( result . terminal ) . toBe ( true ) ;
5030+ expect ( result . phases ?. map ( ( phase ) => phase . phase ) ) . toEqual ( [ "direct-primary" ] ) ;
5031+ expect ( callGateway ) . toHaveBeenCalledTimes ( 1 ) ;
5032+ expect ( queueEmbeddedAgentMessageWithOutcome ) . toHaveBeenCalledTimes ( 1 ) ;
5033+ } ) ;
5034+
5035+ it ( "retries session-file-changed failures without send evidence" , async ( ) => {
5036+ let attempts = 0 ;
5037+ const callGatewaySpy = vi . fn ( ) ;
5038+ const callGateway : typeof runtimeCallGateway = async <
5039+ T = Record < string , unknown > ,
5040+ > ( ) : Promise < T > => {
5041+ callGatewaySpy ( ) ;
5042+ attempts ++ ;
5043+ if ( attempts <= 1 ) {
5044+ throw new Error ( "session file changed while embedded prompt lock was released" ) ;
5045+ }
5046+ return {
5047+ result : {
5048+ payloads : [ { text : "recovered after retry" } ] ,
5049+ } ,
5050+ } as T ;
5051+ } ;
5052+ const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock ( [ "no_active_run" ] ) ;
5053+ const result = await deliverSlackChannelAnnouncement ( {
5054+ callGateway,
5055+ queueEmbeddedAgentMessageWithOutcome,
5056+ sessionId : "requester-session-lock-race-no-evidence" ,
5057+ isActive : true ,
5058+ expectsCompletionMessage : true ,
5059+ directIdempotencyKey : "announce-retry-lock-error-no-evidence" ,
5060+ } ) ;
5061+
5062+ expect ( result . delivered ) . toBe ( true ) ;
5063+ expect ( result . path ) . toBe ( "direct" ) ;
5064+ expect ( callGatewaySpy ) . toHaveBeenCalledTimes ( 2 ) ;
5065+ } ) ;
5066+
5067+ it ( "detects send evidence from OutboundDeliveryError in the error chain" , ( ) => {
5068+ const err = new Error (
5069+ "session file changed while embedded prompt lock was released: /tmp/session.jsonl" ,
5070+ {
5071+ cause : new OutboundDeliveryError ( "outbound delivery failed" , {
5072+ cause : new Error ( "outbound delivery failed" ) ,
5073+ results : [ { channel : "telegram" , messageId : "msg-1" } ] ,
5074+ } ) ,
5075+ } ,
5076+ ) ;
5077+
5078+ expect ( testing . isSessionFileChangedAnnounceError ( err . message ) ) . toBe ( true ) ;
5079+ expect ( testing . hasAnnounceSendEvidence ( err ) ) . toBe ( true ) ;
5080+ } ) ;
5081+
5082+ it ( "classifies session-file-changed error as no-send-evidence when the error chain has no send markers" , ( ) => {
5083+ const err = new Error (
5084+ "session file changed while embedded prompt lock was released: /tmp/session.jsonl" ,
5085+ ) ;
5086+
5087+ expect ( testing . isSessionFileChangedAnnounceError ( err . message ) ) . toBe ( true ) ;
5088+ expect ( testing . hasAnnounceSendEvidence ( err ) ) . toBe ( false ) ;
5089+ } ) ;
5090+
5091+ it ( "detects send evidence from visibleReplySent flag on session-file-changed error" , ( ) => {
5092+ const err = Object . assign (
5093+ new Error ( "session file changed while embedded prompt lock was released: /tmp/session.jsonl" ) ,
5094+ { visibleReplySent : true } ,
5095+ ) ;
5096+
5097+ expect ( testing . hasAnnounceSendEvidence ( err ) ) . toBe ( true ) ;
5098+ } ) ;
5099+
5100+ it ( "detects send evidence from sentBeforeError flag on session-file-changed error" , ( ) => {
5101+ const err = Object . assign (
5102+ new Error ( "session file changed while embedded prompt lock was released: /tmp/session.jsonl" ) ,
5103+ { sentBeforeError : true } ,
5104+ ) ;
5105+
5106+ expect ( testing . hasAnnounceSendEvidence ( err ) ) . toBe ( true ) ;
5107+ } ) ;
5108+
5109+ it ( "detects send evidence recursively through promptError" , ( ) => {
5110+ const takeoverErr = Object . assign (
5111+ new Error ( "session file changed while embedded prompt lock was released: /tmp/session.jsonl" ) ,
5112+ { name : "EmbeddedAttemptSessionTakeoverError" } ,
5113+ ) ;
5114+
5115+ const promptErr = Object . assign ( new Error ( "some model error" ) , { visibleReplySent : true } ) ;
5116+
5117+ const wrapperErr = Object . assign ( new Error ( "some model error" , { cause : takeoverErr } ) , {
5118+ name : "EmbeddedAttemptSessionTakeoverError" ,
5119+ promptError : promptErr ,
5120+ } ) ;
5121+
5122+ expect ( testing . hasAnnounceSendEvidence ( wrapperErr ) ) . toBe ( true ) ;
5123+ expect ( testing . hasSessionFileChangedAnnounceError ( wrapperErr ) ) . toBe ( true ) ;
5124+ } ) ;
49715125} ) ;
0 commit comments