@@ -143,19 +143,17 @@ describe("resolveCronPayloadOutcome", () => {
143143 expect ( result . deliveryPayloads ) . toEqual ( [ { text : "⚠️ ✉️ Message failed" , isError : true } ] ) ;
144144 } ) ;
145145
146- it ( "keeps real trailing errors fatal even when earlier assistant output exists " , ( ) => {
146+ it ( "treats trailing errors as non- fatal when final assistant visible text proves recovery " , ( ) => {
147147 const result = resolveCronPayloadOutcome ( {
148148 payloads : [ { text : "Partial result" } , { text : "model provider unreachable" , isError : true } ] ,
149149 finalAssistantVisibleText : "Partial result" ,
150150 preferFinalAssistantVisibleText : true ,
151151 } ) ;
152152
153- expect ( result . hasFatalErrorPayload ) . toBe ( true ) ;
154- expect ( result . embeddedRunError ) . toBe ( "model provider unreachable" ) ;
155- expect ( result . outputText ) . toBe ( "model provider unreachable" ) ;
156- expect ( result . deliveryPayloads ) . toEqual ( [
157- { text : "model provider unreachable" , isError : true } ,
158- ] ) ;
153+ expect ( result . hasFatalErrorPayload ) . toBe ( false ) ;
154+ expect ( result . embeddedRunError ) . toBeUndefined ( ) ;
155+ expect ( result . outputText ) . toBe ( "Partial result" ) ;
156+ expect ( result . deliveryPayloads ) . toEqual ( [ { text : "Partial result" } ] ) ;
159157 } ) ;
160158
161159 it ( "keeps error payloads fatal when the run also reported a run-level error" , ( ) => {
@@ -323,7 +321,7 @@ describe("resolveCronPayloadOutcome", () => {
323321 expect ( result . deliveryPayloadHasStructuredContent ) . toBe ( true ) ;
324322 } ) ;
325323
326- it ( "returns only the last error payload when all payloads are errors" , ( ) => {
324+ it ( "recovers via finalAssistantVisibleText when all payloads are errors" , ( ) => {
327325 const result = resolveCronPayloadOutcome ( {
328326 payloads : [
329327 { text : "first error" , isError : true } ,
@@ -333,9 +331,10 @@ describe("resolveCronPayloadOutcome", () => {
333331 preferFinalAssistantVisibleText : true ,
334332 } ) ;
335333
336- expect ( result . outputText ) . toBe ( "last error" ) ;
337- expect ( result . deliveryPayloads ) . toEqual ( [ { text : "last error" , isError : true } ] ) ;
338- expect ( result . deliveryPayload ) . toEqual ( { text : "last error" , isError : true } ) ;
334+ expect ( result . hasFatalErrorPayload ) . toBe ( false ) ;
335+ expect ( result . embeddedRunError ) . toBeUndefined ( ) ;
336+ expect ( result . outputText ) . toBe ( "Recovered final answer" ) ;
337+ expect ( result . deliveryPayloads ) . toEqual ( [ { text : "Recovered final answer" } ] ) ;
339338 } ) ;
340339
341340 it ( "keeps multi-payload direct delivery when finalAssistantVisibleText is not preferred" , ( ) => {
@@ -437,4 +436,72 @@ describe("resolveCronPayloadOutcome", () => {
437436 expect ( result . hasFatalErrorPayload ) . toBe ( true ) ;
438437 expect ( result . embeddedRunError ) . toBe ( "Exec failed before SYSTEM_RUN_DENIED could be retried" ) ;
439438 } ) ;
439+
440+ it ( "recovers when finalAssistantVisibleText exists despite unmarked exec error" , ( ) => {
441+ // Reproduces #96255: exec tool rejects a shell redirect ("unknown arg: >"),
442+ // agent retries without the redirect and succeeds, then produces a full
443+ // final report. The exec error payload has no ⚠️ 🛠️ prefix and no
444+ // nonTerminalToolErrorWarning metadata, so no existing gate catches it.
445+ const result = resolveCronPayloadOutcome ( {
446+ payloads : [
447+ { text : "Working on the query..." } ,
448+ { text : "unknown arg: >" , isError : true } ,
449+ // Intermediate tool-output payloads may exist but aren't "deliverable".
450+ ] ,
451+ finalAssistantVisibleText : "Here is the full daily report with all sections." ,
452+ preferFinalAssistantVisibleText : true ,
453+ } ) ;
454+
455+ expect ( result . hasFatalErrorPayload ) . toBe ( false ) ;
456+ expect ( result . hasFatalStructuredErrorPayload ) . toBe ( false ) ;
457+ expect ( result . embeddedRunError ) . toBeUndefined ( ) ;
458+ expect ( result . outputText ) . toBe ( "Here is the full daily report with all sections." ) ;
459+ expect ( result . deliveryPayloads ) . toEqual ( [
460+ { text : "Here is the full daily report with all sections." } ,
461+ ] ) ;
462+ } ) ;
463+
464+ it ( "keeps errors fatal when runLevelError is set despite finalAssistantVisibleText" , ( ) => {
465+ const result = resolveCronPayloadOutcome ( {
466+ payloads : [ { text : "unknown arg: >" , isError : true } ] ,
467+ runLevelError : { kind : "context_overflow" , message : "exceeded context window" } ,
468+ finalAssistantVisibleText : "Partial report before context overflow." ,
469+ } ) ;
470+
471+ expect ( result . hasFatalErrorPayload ) . toBe ( true ) ;
472+ expect ( result . embeddedRunError ) . toContain ( "unknown arg: >" ) ;
473+ } ) ;
474+
475+ it ( "keeps errors fatal when failureSignal is fatalForCron despite finalAssistantVisibleText" , ( ) => {
476+ const result = resolveCronPayloadOutcome ( {
477+ payloads : [ { text : "unknown arg: >" , isError : true } ] ,
478+ failureSignal : {
479+ kind : "execution_denied" ,
480+ message : "approval required" ,
481+ fatalForCron : true ,
482+ } ,
483+ finalAssistantVisibleText : "I tried to run the command." ,
484+ } ) ;
485+
486+ expect ( result . hasFatalErrorPayload ) . toBe ( true ) ;
487+ expect ( result . embeddedRunError ) . toContain ( "unknown arg: >" ) ;
488+ } ) ;
489+
490+ it ( "delivers finalAssistantVisibleText on false-policy channel when recovery gate fires" , ( ) => {
491+ // Regression for ClawSweeper P1: Feishu/Slack channels that do NOT set
492+ // preferFinalAssistantVisibleText should still receive the final report
493+ // when hasRecoveredByFinalAnswer cleared fatal state.
494+ const result = resolveCronPayloadOutcome ( {
495+ payloads : [ { text : "Working on it..." } , { text : "exec failed: unknown arg" , isError : true } ] ,
496+ finalAssistantVisibleText : "Daily report:\n- Item 1\n- Item 2" ,
497+ // Explicitly false: simulates Feishu/Slack channel policy
498+ preferFinalAssistantVisibleText : false ,
499+ } ) ;
500+
501+ expect ( result . hasFatalErrorPayload ) . toBe ( false ) ;
502+ expect ( result . hasFatalStructuredErrorPayload ) . toBe ( false ) ;
503+ expect ( result . embeddedRunError ) . toBeUndefined ( ) ;
504+ expect ( result . outputText ) . toBe ( "Daily report:\n- Item 1\n- Item 2" ) ;
505+ expect ( result . deliveryPayloads ) . toEqual ( [ { text : "Daily report:\n- Item 1\n- Item 2" } ] ) ;
506+ } ) ;
440507} ) ;
0 commit comments