@@ -5277,6 +5277,7 @@ function truncatingCompactor(name: string): Compactor {
52775277
52785278function makeRecordingContextStore ( opts ?: {
52795279 failCommit ?: boolean ;
5280+ failCommitRemaining ?: { n : number } ;
52805281 initialTurns ?: ConversationTurn [ ] ;
52815282} ) : {
52825283 store : ContextStore ;
@@ -5313,6 +5314,10 @@ function makeRecordingContextStore(opts?: {
53135314 if ( opts ?. failCommit === true ) {
53145315 throw new Error ( "commit failed" ) ;
53155316 }
5317+ if ( opts ?. failCommitRemaining !== undefined && opts . failCommitRemaining . n > 0 ) {
5318+ opts . failCommitRemaining . n -= 1 ;
5319+ throw new Error ( "commit failed" ) ;
5320+ }
53165321 commits . push ( {
53175322 message : options . message ,
53185323 turns : [ ...lastWrittenTurns ] ,
@@ -5674,6 +5679,64 @@ describe("createReactor — transform chain ordering and compact action", () =>
56745679 expect ( seenLengths [ 1 ] ) . toBeGreaterThan ( 1 ) ;
56755680 } ) ;
56765681
5682+ test ( "failed compact commit does not replaceTurns stale output on a later infer cycle" , async ( ) => {
5683+ const seed : ConversationTurn [ ] = [
5684+ { role : "user" , content : [ { type : "text" , text : "a" } ] , timestamp : 1 } ,
5685+ { role : "user" , content : [ { type : "text" , text : "b" } ] , timestamp : 2 } ,
5686+ { role : "user" , content : [ { type : "text" , text : "c" } ] , timestamp : 3 } ,
5687+ ] ;
5688+ const recording = makeRecordingContextStore ( {
5689+ failCommitRemaining : { n : 1 } ,
5690+ initialTurns : seed ,
5691+ } ) ;
5692+ let inspectLength = 0 ;
5693+ let messages = 0 ;
5694+ const director : ReactorDirector = {
5695+ async decide ( event , state , caps ) {
5696+ if ( event . type === "message.received" ) {
5697+ messages ++ ;
5698+ if ( messages === 1 ) {
5699+ return caps . compact ( "tail-only" , "explicit-test" ) ;
5700+ }
5701+ if ( messages === 2 ) {
5702+ return caps . infer ( ) ;
5703+ }
5704+ inspectLength = state . turns . length ;
5705+ return caps . done ( ) ;
5706+ }
5707+ if ( event . type === "inference.done" ) {
5708+ return caps . wait ( ) ;
5709+ }
5710+ return caps . done ( ) ;
5711+ } ,
5712+ } ;
5713+ const { reactor, waitFor } = createDirectReactor ( {
5714+ contextStore : recording . store ,
5715+ director,
5716+ compactors : { "tail-only" : truncatingCompactor ( "tail-only" ) } ,
5717+ inferenceRunner : mockInferenceRunner ( "live-after-failed-compact" ) ,
5718+ } ) ;
5719+ reactor . start ( ) ;
5720+ reactor . deliver ( makeInboundMessage ( ) ) ;
5721+ setTimeout ( ( ) => reactor . deliver ( makeInboundMessage ( ) ) , 30 ) ;
5722+ setTimeout ( ( ) => reactor . deliver ( makeInboundMessage ( ) ) , 80 ) ;
5723+ await waitFor ( "reactor.done" ) ;
5724+
5725+ const liveWrites = recording . writeTurnsCalls . filter ( ( turns ) =>
5726+ turns . some (
5727+ ( turn ) =>
5728+ turn . role === "assistant" &&
5729+ turn . content . some ( ( b ) => b . type === "text" && b . text === "live-after-failed-compact" ) ,
5730+ ) ,
5731+ ) ;
5732+ expect ( liveWrites . length ) . toBeGreaterThan ( 0 ) ;
5733+ expect ( liveWrites . some ( ( turns ) => turns . length === 1 ) ) . toBe ( false ) ;
5734+ expect ( inspectLength ) . toBeGreaterThan ( 1 ) ;
5735+ const inferCommit = recording . commits . find ( ( c ) => c . message . startsWith ( "Cycle: inferred" ) ) ;
5736+ expect ( inferCommit ) . toBeDefined ( ) ;
5737+ expect ( inferCommit ?. turns . length ) . toBeGreaterThan ( 1 ) ;
5738+ } ) ;
5739+
56775740 test ( "compact for an unknown name emits a fatal error and shuts down" , async ( ) => {
56785741 const recording = makeRecordingContextStore ( ) ;
56795742 const { reactor, events, waitFor } = createDirectReactor ( {
0 commit comments