@@ -10,8 +10,13 @@ import {
1010 COMPACTION_CONTINUATION_EVENT ,
1111 OPERATOR_COMPACT_REASON ,
1212 createCompactionGovernor ,
13+ foldNonConvergedNotice ,
1314 stickyExtraInstructionsFromRecords ,
1415} from "./compaction.js" ;
16+ import {
17+ COMPACTION_FOLD_NONCONVERGED_EVENT ,
18+ createChatDirector ,
19+ } from "./director.js" ;
1520import {
1621 compactionWideResumeDeltaFor ,
1722 compactionThresholdFor ,
@@ -157,7 +162,10 @@ describe("compaction governor", () => {
157162 governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
158163 ) . toBeNull ( ) ;
159164
160- governor . noteInferenceDone ( inferenceDone ( overThreshold ) , turnsOfLength ( 1 , 1 ) ) ;
165+ governor . noteInferenceDone (
166+ inferenceDone ( overThreshold ) ,
167+ turnsOfLength ( 1 , 1 ) ,
168+ ) ;
161169 expect (
162170 governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
163171 ) . toBeNull ( ) ;
@@ -705,7 +713,10 @@ describe("compaction governor", () => {
705713
706714 test ( "manual compact no-ops below the compactor floor" , ( ) => {
707715 const governor = createCompactionGovernor ( undefined ) ;
708- governor . noteInferenceDone ( inferenceDone ( overThreshold ) , turnsOfLength ( 1 , 1 ) ) ;
716+ governor . noteInferenceDone (
717+ inferenceDone ( overThreshold ) ,
718+ turnsOfLength ( 1 , 1 ) ,
719+ ) ;
709720 expect ( governor . requestManual ( "focus on tests" ) ) . toBe ( "noop" ) ;
710721 expect ( governor . extraInstructions ) . toBeUndefined ( ) ;
711722 expect (
@@ -844,6 +855,103 @@ describe("post-compact above-threshold latch (CL-9006)", () => {
844855 expect ( governor . foldNonConverged ) . toBe ( true ) ;
845856 } ) ;
846857
858+ test ( "foldNonConvergedNotice names the still-over fold for the operator" , ( ) => {
859+ expect ( foldNonConvergedNotice ( ) ) . toMatch ( / d i d n o t r e d u c e o c c u p a n c y / i) ;
860+ expect ( foldNonConvergedNotice ( ) ) . toMatch ( / p a u s e d / i) ;
861+ } ) ;
862+
863+ test ( "ChatDirector emits the non-convergence notice once after a still-over fold" , async ( ) => {
864+ const director = createChatDirector ( "sys" , [ ] , { } ) ;
865+ const caps : ReactorCapabilities = {
866+ infer : ( ) => ( { type : "infer" } ) ,
867+ executeTools : ( ) => ( { type : "execute_tools" , calls : [ ] } ) ,
868+ suspend : ( gate ) => ( { type : "suspend" , gate } ) ,
869+ fork : ( mode , forkId ) => ( { type : "fork" , mode, forkId } ) ,
870+ emit : ( eventType , data ) => ( { type : "emit" , eventType, data } ) ,
871+ reply : ( content : string ) => ( { type : "reply" , content } ) ,
872+ checkpoint : ( message = "" ) => ( { type : "checkpoint" , message } ) ,
873+ compact : ( compactor , reason ) => ( { type : "compact" , compactor, reason } ) ,
874+ wait : ( ) => ( { type : "wait" } ) ,
875+ done : ( ) => ( { type : "done" } ) ,
876+ } ;
877+ const manyTurns = {
878+ turns : Array . from ( { length : 8 } , ( ) => ( {
879+ role : "assistant" as const ,
880+ content : [ ] ,
881+ model : "test-model" ,
882+ timestamp : 0 ,
883+ } ) ) ,
884+ activeForks : [ ] ,
885+ pendingOperations : [ ] ,
886+ activeGates : [ ] ,
887+ tokenUsage : usage ( overThreshold ) ,
888+ lastCycleUsage : null ,
889+ lastCycleSource : null ,
890+ sessionId : "test-session" ,
891+ } ;
892+ const done = ( input : number ) : ReactorInboundEvent =>
893+ ( {
894+ type : "inference.done" ,
895+ turn : {
896+ role : "assistant" ,
897+ content : [
898+ {
899+ type : "tool_call" ,
900+ id : "call-1" ,
901+ name : "read_file" ,
902+ arguments : { path : "x.ts" } ,
903+ } ,
904+ ] ,
905+ model : "test-model" ,
906+ timestamp : 0 ,
907+ } ,
908+ usage : usage ( input ) ,
909+ source : {
910+ sourceId : "test" ,
911+ provider : "openai" ,
912+ model : "test-model" ,
913+ } ,
914+ } ) as ReactorInboundEvent ;
915+ const toolDoneEvent = {
916+ type : "tool.done" ,
917+ result : { callId : "call-1" , content : "ok" , isError : false } ,
918+ } as ReactorInboundEvent ;
919+
920+ await director . decide ( done ( overThreshold ) , manyTurns as never , caps ) ;
921+ await director . decide ( toolDoneEvent , manyTurns as never , caps ) ;
922+ const post = await director . decide (
923+ done ( overThreshold ) ,
924+ manyTurns as never ,
925+ caps ,
926+ ) ;
927+ const arr = Array . isArray ( post ) ? post : [ post ] ;
928+ const notices = arr . filter (
929+ ( a ) =>
930+ a . type === "emit" &&
931+ "eventType" in a &&
932+ a . eventType === COMPACTION_FOLD_NONCONVERGED_EVENT ,
933+ ) ;
934+ expect ( notices ) . toHaveLength ( 1 ) ;
935+ expect ( notices [ 0 ] ) . toMatchObject ( {
936+ data : { notice : foldNonConvergedNotice ( ) } ,
937+ } ) ;
938+
939+ const again = await director . decide (
940+ done ( overThreshold + 1 ) ,
941+ manyTurns as never ,
942+ caps ,
943+ ) ;
944+ const againArr = Array . isArray ( again ) ? again : [ again ] ;
945+ expect (
946+ againArr . filter (
947+ ( a ) =>
948+ a . type === "emit" &&
949+ "eventType" in a &&
950+ a . eventType === COMPACTION_FOLD_NONCONVERGED_EVENT ,
951+ ) ,
952+ ) . toHaveLength ( 0 ) ;
953+ } ) ;
954+
847955 test ( "the consecutive-compact cap holds across tool-call occupancy" , ( ) => {
848956 const governor = createCompactionGovernor ( ( ) => undefined ) ;
849957 governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
@@ -1207,7 +1315,9 @@ describe("handoff arming (/handoff)", () => {
12071315 {
12081316 title : "noop then cancelManual does not wipe extras from a prior fold" ,
12091317 fire : "idle" ,
1210- midRequests : [ { turns : turnsOfLength ( 1 , 1 ) , text : "wipe this" , result : "noop" } ] ,
1318+ midRequests : [
1319+ { turns : turnsOfLength ( 1 , 1 ) , text : "wipe this" , result : "noop" } ,
1320+ ] ,
12111321 } ,
12121322 {
12131323 title :
0 commit comments