@@ -1864,6 +1864,107 @@ describe("compaction-safeguard recent-turn preservation", () => {
18641864 expect ( JSON . stringify ( messages [ 0 ] ) ) . toContain ( "Old duplicated section" ) ;
18651865 } ) ;
18661866
1867+ it ( "falls back to LLM when provider throws a provider-side AbortError with signal not aborted" , async ( ) => {
1868+ // Reproduce the undici AbortError("This operation was aborted") shape that
1869+ // arrives when the compaction provider's HTTP connection drops mid-stream while
1870+ // the caller has NOT yet fired their abort signal. Before the fix,
1871+ // isAbortError() matched this shape so tryProviderSummarize rethrew and the
1872+ // extension runner swallowed the error — the LLM fallback path was skipped.
1873+ mockSummarizeInStages . mockReset ( ) ;
1874+ mockSummarizeInStages . mockResolvedValue ( "llm fallback summary" ) ;
1875+
1876+ const providerAbortErr = Object . assign ( new Error ( "This operation was aborted" ) , {
1877+ name : "AbortError" ,
1878+ } ) ;
1879+ const failingProviderSummarize = vi . fn ( ) . mockRejectedValue ( providerAbortErr ) ;
1880+ registerCompactionProvider ( {
1881+ id : "disconnecting-provider" ,
1882+ label : "Disconnecting Provider" ,
1883+ summarize : failingProviderSummarize ,
1884+ } ) ;
1885+
1886+ const sessionManager = stubSessionManager ( ) ;
1887+ const model = createAnthropicModelFixture ( ) ;
1888+ setCompactionSafeguardRuntime ( sessionManager , {
1889+ provider : "disconnecting-provider" ,
1890+ model,
1891+ recentTurnsPreserve : 0 ,
1892+ } ) ;
1893+
1894+ const event = {
1895+ preparation : {
1896+ messagesToSummarize : [
1897+ { role : "user" , content : "older context" , timestamp : 1 } ,
1898+ { role : "assistant" , content : "older reply" , timestamp : 2 } as unknown as AgentMessage ,
1899+ ] ,
1900+ turnPrefixMessages : [ ] as AgentMessage [ ] ,
1901+ firstKeptEntryId : "entry-1" ,
1902+ tokensBefore : 1_500 ,
1903+ fileOps : {
1904+ read : [ ] ,
1905+ edited : [ ] ,
1906+ written : [ ] ,
1907+ } ,
1908+ settings : { reserveTokens : 4_000 } ,
1909+ } ,
1910+ customInstructions : "" ,
1911+ signal : new AbortController ( ) . signal , // not aborted
1912+ } ;
1913+ const { result } = await runCompactionScenario ( { sessionManager, event, apiKey : "key" } ) ;
1914+
1915+ // Provider failure → LLM fallback ran, not { cancel: true }.
1916+ expect ( result . cancel ) . not . toBe ( true ) ;
1917+ expect ( mockSummarizeInStages ) . toHaveBeenCalled ( ) ;
1918+ } ) ;
1919+
1920+ it ( "propagates provider AbortError and cancels when caller signal is already aborted" , async ( ) => {
1921+ mockSummarizeInStages . mockReset ( ) ;
1922+
1923+ const providerAbortErr = Object . assign ( new Error ( "This operation was aborted" ) , {
1924+ name : "AbortError" ,
1925+ } ) ;
1926+ const failingProviderSummarize = vi . fn ( ) . mockRejectedValue ( providerAbortErr ) ;
1927+ registerCompactionProvider ( {
1928+ id : "aborted-provider" ,
1929+ label : "Aborted Provider" ,
1930+ summarize : failingProviderSummarize ,
1931+ } ) ;
1932+
1933+ const controller = new AbortController ( ) ;
1934+ controller . abort ( ) ;
1935+
1936+ const sessionManager = stubSessionManager ( ) ;
1937+ setCompactionSafeguardRuntime ( sessionManager , {
1938+ provider : "aborted-provider" ,
1939+ } ) ;
1940+
1941+ const event = {
1942+ preparation : {
1943+ messagesToSummarize : [
1944+ { role : "user" , content : "older context" , timestamp : 1 } ,
1945+ ] as AgentMessage [ ] ,
1946+ turnPrefixMessages : [ ] as AgentMessage [ ] ,
1947+ firstKeptEntryId : "entry-1" ,
1948+ tokensBefore : 1_500 ,
1949+ fileOps : {
1950+ read : [ ] ,
1951+ edited : [ ] ,
1952+ written : [ ] ,
1953+ } ,
1954+ settings : { reserveTokens : 4_000 } ,
1955+ } ,
1956+ customInstructions : "" ,
1957+ signal : controller . signal , // already aborted
1958+ } ;
1959+
1960+ await expect (
1961+ runCompactionScenario ( { sessionManager, event, apiKey : "key" } ) ,
1962+ ) . rejects . toMatchObject ( { name : "AbortError" } ) ;
1963+
1964+ // Caller abort is terminal — LLM fallback should not have run.
1965+ expect ( mockSummarizeInStages ) . not . toHaveBeenCalled ( ) ;
1966+ } ) ;
1967+
18671968 it ( "passes compaction instructions to providers and preserves suffix context" , async ( ) => {
18681969 mockSummarizeInStages . mockReset ( ) ;
18691970 const providerSummarize = vi . fn ( ) . mockResolvedValue ( "provider summary body" ) ;
0 commit comments