@@ -31,8 +31,10 @@ import type {
3131 SessionSummary ,
3232 TurnRecord ,
3333} from '@maka/core/session' ;
34+ import type { ContextCompactResult } from '@maka/runtime-host/protocol' ;
3435import {
3536 createFakeWorkbarServices ,
37+ dispatchQuoteCompanionInput ,
3638 useQuoteCompanion ,
3739 sessionHasExactModelChoice ,
3840 WorkbarServicesProvider ,
@@ -136,6 +138,7 @@ async function renderProbe(
136138 onStop ?: ( stop : ( ) => Promise < void > ) => void ;
137139 onSetPermissionMode ?: ( set : ( mode : PermissionMode ) => Promise < boolean > ) => void ;
138140 confirmBypass ?: ( ) => Promise < boolean > ;
141+ onContextCompactionError ?: ( sessionId : string , error : unknown ) => void ;
139142 pendingQuotes ?: readonly StagedCompanionQuote [ ] ;
140143 onQuotesConsumed ?: ( snapshot : CompanionQuoteSnapshot ) => void ;
141144 } = { } ,
@@ -159,6 +162,7 @@ async function renderProbe(
159162 onSteer : options . onSteer ,
160163 onStop : options . onStop ,
161164 onSetPermissionMode : options . onSetPermissionMode ,
165+ onContextCompactionError : options . onContextCompactionError ,
162166 pendingQuotes : options . pendingQuotes ,
163167 onQuotesConsumed : options . onQuotesConsumed ,
164168 sourceSession : options . sourceSession ,
@@ -191,6 +195,7 @@ async function renderOwnershipProbe(
191195 onQuotesConsumed ?: ( snapshot : CompanionQuoteSnapshot ) => void ;
192196 sourceSession ?: SessionSummary ;
193197 modelChoices ?: readonly ChatModelChoice [ ] ;
198+ onContextCompactionError ?: ( sessionId : string , error : unknown ) => void ;
194199 } = { } ,
195200) {
196201 let send ! : ( text : string ) => Promise < boolean > ;
@@ -233,6 +238,16 @@ async function renderOwnershipProbe(
233238 } ;
234239}
235240
241+ async function commitIdleCompanion (
242+ rendered : Awaited < ReturnType < typeof renderOwnershipProbe > > ,
243+ ) : Promise < void > {
244+ await act ( async ( ) => {
245+ assert . equal ( await rendered . send ( 'prepare side conversation' ) , false ) ;
246+ await Promise . resolve ( ) ;
247+ } ) ;
248+ await awaitCompanion ( rendered . container ) ;
249+ }
250+
236251const REBOUND_MODEL : Partial < SessionSummary > = {
237252 llmConnectionId : 'connection-2' ,
238253 llmConnectionSlug : 'openai-2' ,
@@ -361,6 +376,319 @@ test('first send after a completed turn forks through the settled turn', async (
361376 assert . equal ( probe . getAttribute ( 'data-error' ) , '' ) ;
362377} ) ;
363378
379+ test ( 'dispatches /compact to the committed companion fork without sending model input' , async ( ) => {
380+ const compactCalls : string [ ] = [ ] ;
381+ let sendCalls = 0 ;
382+ let steerCalls = 0 ;
383+ const rendered = await renderOwnershipProbe ( {
384+ compact : async ( sessionId ) => {
385+ compactCalls . push ( sessionId ) ;
386+ return {
387+ kind : 'finished' as const ,
388+ turn : {
389+ sessionId,
390+ turnId : 'compact-turn' ,
391+ runId : 'compact-run' ,
392+ status : 'completed' as const ,
393+ terminalEventId : 'compact-complete' ,
394+ contextCompactionOutcome : { kind : 'unchanged' as const , reason : 'already_current' } ,
395+ } ,
396+ outcome : { kind : 'unchanged' as const , reason : 'already_current' } ,
397+ } ;
398+ } ,
399+ send : async ( ) => {
400+ sendCalls += 1 ;
401+ return { ok : false as const , reason : 'seed only' } ;
402+ } ,
403+ steer : async ( ) => {
404+ steerCalls += 1 ;
405+ return { kind : 'started' as const , turnId : 'unexpected-steer' } ;
406+ } ,
407+ } ) ;
408+
409+ await commitIdleCompanion ( rendered ) ;
410+ sendCalls = 0 ;
411+ assert . equal ( await rendered . send ( ' /compact ' ) , true ) ;
412+ assert . deepEqual ( compactCalls , [ 'side-conversation' ] ) ;
413+ assert . equal ( sendCalls , 0 ) ;
414+ assert . equal ( steerCalls , 0 ) ;
415+ } ) ;
416+
417+ test ( 'dispatches the exact /compact Composer command before steering or ordinary send' , async ( ) => {
418+ const calls : string [ ] = [ ] ;
419+ assert . equal (
420+ await dispatchQuoteCompanionInput ( {
421+ text : ' /compact ' ,
422+ streaming : true ,
423+ compact : async ( ) => {
424+ calls . push ( 'compact' ) ;
425+ return true ;
426+ } ,
427+ steer : async ( ) => {
428+ calls . push ( 'steer' ) ;
429+ return true ;
430+ } ,
431+ send : async ( ) => {
432+ calls . push ( 'send' ) ;
433+ return true ;
434+ } ,
435+ } ) ,
436+ true ,
437+ ) ;
438+ assert . deepEqual ( calls , [ 'compact' ] ) ;
439+ } ) ;
440+
441+ test ( 'keeps an async companion compaction exclusive until its terminal event' , async ( ) => {
442+ let compactCalls = 0 ;
443+ let sendCalls = 0 ;
444+ const rendered = await renderOwnershipProbe ( {
445+ compact : async ( sessionId ) => {
446+ compactCalls += 1 ;
447+ return {
448+ kind : 'started' as const ,
449+ turn : {
450+ sessionId,
451+ turnId : 'compact-turn' ,
452+ runId : 'compact-run' ,
453+ status : 'running' as const ,
454+ } ,
455+ } ;
456+ } ,
457+ send : async ( ) => {
458+ sendCalls += 1 ;
459+ return { ok : false as const , reason : 'seed only' } ;
460+ } ,
461+ } ) ;
462+
463+ await commitIdleCompanion ( rendered ) ;
464+ sendCalls = 0 ;
465+ assert . equal ( await rendered . send ( '/compact' ) , true ) ;
466+ assert . equal ( await rendered . send ( 'ordinary question' ) , false ) ;
467+ assert . equal ( compactCalls , 1 ) ;
468+ assert . equal ( sendCalls , 0 ) ;
469+ } ) ;
470+
471+ test ( 'releases an async companion compaction after a Host interruption' , async ( ) => {
472+ let compactCalls = 0 ;
473+ const compactionErrors : Array < { sessionId : string ; error : unknown } > = [ ] ;
474+ const rendered = await renderOwnershipProbe (
475+ {
476+ compact : async ( sessionId ) => {
477+ compactCalls += 1 ;
478+ return {
479+ kind : 'started' as const ,
480+ turn : {
481+ sessionId,
482+ turnId : `compact-turn-${ compactCalls } ` ,
483+ runId : `compact-run-${ compactCalls } ` ,
484+ status : 'running' as const ,
485+ } ,
486+ } ;
487+ } ,
488+ } ,
489+ {
490+ onContextCompactionError : ( sessionId , error ) => {
491+ compactionErrors . push ( { sessionId, error } ) ;
492+ } ,
493+ } ,
494+ ) ;
495+
496+ await commitIdleCompanion ( rendered ) ;
497+ assert . equal ( await rendered . send ( '/compact' ) , true ) ;
498+ assert . equal ( await rendered . send ( '/compact' ) , false ) ;
499+ await act ( async ( ) => {
500+ rendered . emit ( {
501+ type : 'abort' ,
502+ id : 'compact-aborted' ,
503+ turnId : 'compact-turn-1' ,
504+ ts : 1 ,
505+ reason : 'crash' ,
506+ } ) ;
507+ await Promise . resolve ( ) ;
508+ } ) ;
509+
510+ assert . equal ( await rendered . send ( '/compact' ) , true ) ;
511+ assert . equal ( compactCalls , 2 ) ;
512+ assert . equal ( compactionErrors . length , 1 ) ;
513+ assert . equal ( compactionErrors [ 0 ] ?. sessionId , 'side-conversation' ) ;
514+ assert . equal ( ( compactionErrors [ 0 ] ?. error as SessionEvent | undefined ) ?. type , 'abort' ) ;
515+ } ) ;
516+
517+ test ( 'does not settle a pending companion compaction from another turn outcome' , async ( ) => {
518+ const pendingCompact = deferred < ContextCompactResult > ( ) ;
519+ let compactCalls = 0 ;
520+ const rendered = await renderOwnershipProbe ( {
521+ compact : async ( sessionId ) => {
522+ compactCalls += 1 ;
523+ if ( compactCalls === 1 ) return pendingCompact . promise ;
524+ return {
525+ kind : 'finished' as const ,
526+ turn : {
527+ sessionId,
528+ turnId : 'compact-turn-after-guard' ,
529+ runId : 'compact-run-after-guard' ,
530+ status : 'completed' as const ,
531+ terminalEventId : 'compact-complete-after-guard' ,
532+ contextCompactionOutcome : { kind : 'unchanged' as const , reason : 'already_current' } ,
533+ } ,
534+ outcome : { kind : 'unchanged' as const , reason : 'already_current' } ,
535+ } ;
536+ } ,
537+ } ) ;
538+
539+ await commitIdleCompanion ( rendered ) ;
540+ let compactResult ! : Promise < boolean > ;
541+ await act ( async ( ) => {
542+ compactResult = rendered . send ( '/compact' ) ;
543+ await Promise . resolve ( ) ;
544+ } ) ;
545+ await act ( async ( ) => {
546+ rendered . emit ( {
547+ type : 'complete' ,
548+ id : 'unrelated-complete' ,
549+ turnId : 'unrelated-turn' ,
550+ ts : 1 ,
551+ stopReason : 'end_turn' ,
552+ contextCompactionOutcome : { kind : 'unchanged' , reason : 'already_current' } ,
553+ } ) ;
554+ await Promise . resolve ( ) ;
555+ } ) ;
556+
557+ assert . equal ( await rendered . send ( '/compact' ) , false ) ;
558+ pendingCompact . resolve ( {
559+ kind : 'started' ,
560+ turn : {
561+ sessionId : 'side-conversation' ,
562+ turnId : 'compact-turn-unrelated-guard' ,
563+ runId : 'compact-run-unrelated-guard' ,
564+ status : 'running' ,
565+ } ,
566+ } ) ;
567+ assert . equal ( await compactResult , true ) ;
568+ assert . equal ( compactCalls , 1 ) ;
569+
570+ await act ( async ( ) => {
571+ rendered . emit ( {
572+ type : 'complete' ,
573+ id : 'compact-complete' ,
574+ turnId : 'compact-turn-unrelated-guard' ,
575+ ts : 2 ,
576+ stopReason : 'end_turn' ,
577+ contextCompactionOutcome : { kind : 'unchanged' , reason : 'already_current' } ,
578+ } ) ;
579+ await Promise . resolve ( ) ;
580+ } ) ;
581+ assert . equal ( await rendered . send ( '/compact' ) , true ) ;
582+ assert . equal ( compactCalls , 2 ) ;
583+ } ) ;
584+
585+ test ( 'clears a failed companion compaction request so it can be retried' , async ( ) => {
586+ let compactCalls = 0 ;
587+ const rendered = await renderOwnershipProbe ( {
588+ compact : async ( sessionId ) => {
589+ compactCalls += 1 ;
590+ if ( compactCalls === 1 ) throw new Error ( 'temporary compact failure' ) ;
591+ return {
592+ kind : 'finished' as const ,
593+ turn : {
594+ sessionId,
595+ turnId : 'compact-retry-turn' ,
596+ runId : 'compact-retry-run' ,
597+ status : 'completed' as const ,
598+ terminalEventId : 'compact-retry-complete' ,
599+ contextCompactionOutcome : { kind : 'unchanged' as const , reason : 'already_current' } ,
600+ } ,
601+ outcome : { kind : 'unchanged' as const , reason : 'already_current' } ,
602+ } ;
603+ } ,
604+ } ) ;
605+
606+ await commitIdleCompanion ( rendered ) ;
607+ assert . equal ( await rendered . send ( '/compact' ) , false ) ;
608+ assert . equal ( await rendered . send ( '/compact' ) , true ) ;
609+ assert . equal ( compactCalls , 2 ) ;
610+ } ) ;
611+
612+ test ( 'rejects /compact while the companion is running without consuming staged quotes' , async ( ) => {
613+ const pendingSend = deferred < { ok : true ; turnId : string } > ( ) ;
614+ let compactCalls = 0 ;
615+ const consumed : CompanionQuoteSnapshot [ ] = [ ] ;
616+ const rendered = await renderOwnershipProbe (
617+ {
618+ compact : async ( ) => {
619+ compactCalls += 1 ;
620+ throw new Error ( 'compact should not run while busy' ) ;
621+ } ,
622+ send : ( ) => pendingSend . promise ,
623+ } ,
624+ {
625+ pendingQuotes : [ { id : 'quote-1' , value : { text : 'quoted context' } } ] ,
626+ onQuotesConsumed : ( snapshot ) => consumed . push ( snapshot ) ,
627+ } ,
628+ ) ;
629+
630+ let sendResult ! : Promise < boolean > ;
631+ await act ( async ( ) => {
632+ sendResult = rendered . send ( 'ordinary question' ) ;
633+ await Promise . resolve ( ) ;
634+ } ) ;
635+ assert . equal ( await rendered . send ( '/compact' ) , false ) ;
636+ assert . equal ( compactCalls , 0 ) ;
637+ assert . deepEqual ( consumed , [ ] ) ;
638+
639+ await act ( async ( ) => {
640+ pendingSend . resolve ( { ok : true , turnId : 'running-turn' } ) ;
641+ assert . equal ( await sendResult , true ) ;
642+ } ) ;
643+ } ) ;
644+
645+ test ( 'rejects /compact while the companion fork is preparing' , async ( ) => {
646+ const pendingFork = deferred < SessionSummary > ( ) ;
647+ let compactCalls = 0 ;
648+ let branchStarted = false ;
649+ const rendered = await renderOwnershipProbe ( {
650+ branchFromTurn : async ( ) => {
651+ branchStarted = true ;
652+ return { ok : true as const , session : await pendingFork . promise } ;
653+ } ,
654+ compact : async ( ) => {
655+ compactCalls += 1 ;
656+ throw new Error ( 'compact should not run before fork commit' ) ;
657+ } ,
658+ } ) ;
659+
660+ let sendResult ! : Promise < boolean > ;
661+ await act ( async ( ) => {
662+ sendResult = rendered . send ( 'prepare pending fork' ) ;
663+ await Promise . resolve ( ) ;
664+ } ) ;
665+ await waitUntil ( ( ) => branchStarted ) ;
666+ assert . equal ( await rendered . send ( '/compact' ) , false ) ;
667+ assert . equal ( compactCalls , 0 ) ;
668+ await act ( async ( ) => {
669+ pendingFork . resolve ( session ( 'side-conversation' ) ) ;
670+ assert . equal ( await sendResult , false ) ;
671+ } ) ;
672+ } ) ;
673+
674+ test ( 'rejects /compact for an archived companion fork without invoking Runtime Host' , async ( ) => {
675+ let compactCalls = 0 ;
676+ const rendered = await renderOwnershipProbe ( {
677+ branchFromTurn : async ( ) => ( {
678+ ok : true as const ,
679+ session : session ( 'side-conversation' , { isArchived : true } ) ,
680+ } ) ,
681+ compact : async ( ) => {
682+ compactCalls += 1 ;
683+ throw new Error ( 'compact should not run for an archived fork' ) ;
684+ } ,
685+ } ) ;
686+
687+ await commitIdleCompanion ( rendered ) ;
688+ assert . equal ( await rendered . send ( '/compact' ) , false ) ;
689+ assert . equal ( compactCalls , 0 ) ;
690+ } ) ;
691+
364692test ( 'does not fork on mount or when the source Session object refreshes' , async ( ) => {
365693 let branchCount = 0 ;
366694 const { container, root, services } = await renderProbe (
@@ -1610,6 +1938,7 @@ function QuoteCompanionOwnershipProbe(props: {
16101938 onSteer ?: ( steer : ( text : string ) => Promise < boolean > ) => void ;
16111939 onStop ?: ( stop : ( ) => Promise < void > ) => void ;
16121940 onSetPermissionMode ?: ( set : ( mode : PermissionMode ) => Promise < boolean > ) => void ;
1941+ onContextCompactionError ?: ( sessionId : string , error : unknown ) => void ;
16131942 pendingQuotes ?: readonly StagedCompanionQuote [ ] ;
16141943 onQuotesConsumed ?: ( snapshot : CompanionQuoteSnapshot ) => void ;
16151944 sourceSession ?: SessionSummary ;
@@ -1624,6 +1953,7 @@ function QuoteCompanionOwnershipProbe(props: {
16241953 locale : 'en' ,
16251954 onQuotesConsumed : props . onQuotesConsumed ?? ( ( ) => undefined ) ,
16261955 confirmBypass : async ( ) => true ,
1956+ onContextCompactionError : props . onContextCompactionError ,
16271957 } ) ;
16281958 props . onSend ( companion . send ) ;
16291959 props . onSteer ?.( companion . steer ) ;
0 commit comments