@@ -4,6 +4,8 @@ import fs from "node:fs";
44import os from "node:os" ;
55import path from "node:path" ;
66import type { InboundMessage } from "@intx/types/runtime" ;
7+ import { base64Encode } from "@intx/types" ;
8+ import { createInboundTurn } from "@intx/inference" ;
79import { CREDENTIAL_REDACTION } from "../plugins/tool-result-secret-scrub.js" ;
810import {
911 admitPrimaryInboundMessage ,
@@ -180,7 +182,11 @@ describe("primary message admission", () => {
180182 expect ( occurrences ) . toHaveLength ( 1 ) ;
181183 expect ( occurrences [ 0 ] ! . kind ) . toBe ( "user_message" ) ;
182184 const archived = await archive . readAuthorizedPayload ( occurrences [ 0 ] ! . occurrenceId ) ;
183- expect ( archived ) . toBe ( admitted ) ;
185+ const history = createInboundTurn ( delivered [ 0 ] ! ) ;
186+ const historyText = history ?. content . find ( ( block ) => block . type === "text" ) ;
187+ expect ( historyText ?. type === "text" ? historyText . text : undefined ) . toBe ( archived ) ;
188+ expect ( archived . startsWith ( "[From: user@local]\n\n" ) ) . toBe ( true ) ;
189+ expect ( archived . endsWith ( admitted ) ) . toBe ( true ) ;
184190 } ) ;
185191
186192 test ( "send(string) admits and archives like InboundMessage" , async ( ) => {
@@ -217,7 +223,9 @@ describe("primary message admission", () => {
217223 expect ( sent [ 0 ] ) . not . toContain ( secret ) ;
218224 const occurrences = await archive . listOccurrences ( ) ;
219225 expect ( occurrences ) . toHaveLength ( 1 ) ;
220- expect ( await archive . readAuthorizedPayload ( occurrences [ 0 ] ! . occurrenceId ) ) . toBe ( sent [ 0 ] ! ) ;
226+ expect ( await archive . readAuthorizedPayload ( occurrences [ 0 ] ! . occurrenceId ) ) . toBe (
227+ `[From: user@local]\n\n${ sent [ 0 ] ! } ` ,
228+ ) ;
221229 } ) ;
222230} ) ;
223231
@@ -712,7 +720,8 @@ describe("wrapCompactorWithCompletenessGate", () => {
712720 const { wrapCompactorWithCompletenessGate } = await import ( "./compaction-archive.js" ) ;
713721 const { archive } = memoryArchive ( ) ;
714722 const wrapped = wrapCompactorWithCompletenessGate ( truncating ( "pruning-compactor" ) , archive ) ;
715- const png = "iVBORw0KGgo=" ;
723+ const pngBytes = new Uint8Array ( [ 137 , 80 , 78 , 71 , 13 , 10 , 26 , 10 , 1 , 2 , 3 ] ) ;
724+ const png = base64Encode ( pngBytes ) ;
716725 const turns : import ( "@intx/types/runtime" ) . ConversationTurn [ ] = [
717726 {
718727 role : "user" ,
@@ -735,16 +744,140 @@ describe("wrapCompactorWithCompletenessGate", () => {
735744 expect ( blocked . output ) . toBe ( turns ) ;
736745 expect ( blocked . record . reason ) . toBe ( "incomplete-evidence-archive" ) ;
737746
747+ const agent = {
748+ deliver ( _message : InboundMessage ) {
749+ /* admission archives; history is the turns above */
750+ } ,
751+ async send ( content : string | InboundMessage ) {
752+ return { ok : true as const , content } ;
753+ } ,
754+ } ;
755+ const admitted = createPrimaryDeliveryAdmission ( agent , archive ) ;
756+ await admitted . send (
757+ inbound ( {
758+ attachments : [ { name : "shot.png" , contentType : "image/png" , data : pngBytes } ] ,
759+ } ) ,
760+ ) ;
761+ const allowed = await wrapped . apply ( turns , ctx ) ;
762+ expect ( allowed . output ) . toHaveLength ( 1 ) ;
763+ expect ( allowed . record . reason ) . toBe ( "compact" ) ;
764+ } ) ;
765+
766+ test ( "dropped list_dir and write_file results fail-close until archived" , async ( ) => {
767+ const { wrapCompactorWithCompletenessGate } = await import ( "./compaction-archive.js" ) ;
768+ const { archive } = memoryArchive ( ) ;
769+ const wrapped = wrapCompactorWithCompletenessGate ( truncating ( "pruning-compactor" ) , archive ) ;
770+ const turns : import ( "@intx/types/runtime" ) . ConversationTurn [ ] = [
771+ {
772+ role : "user" ,
773+ content : [ { type : "text" , text : "do work" } ] ,
774+ timestamp : 1 ,
775+ } ,
776+ {
777+ role : "assistant" ,
778+ content : [ { type : "tool_call" , id : "ld" , name : "list_dir" , arguments : { path : "." } } ] ,
779+ timestamp : 2 ,
780+ } ,
781+ {
782+ role : "user" ,
783+ content : [
784+ { type : "tool_result" , callId : "ld" , content : [ { type : "text" , text : "src/\n" } ] } ,
785+ ] ,
786+ timestamp : 3 ,
787+ } ,
788+ {
789+ role : "assistant" ,
790+ content : [ { type : "tool_call" , id : "wf" , name : "write_file" , arguments : { path : "a.ts" } } ] ,
791+ timestamp : 4 ,
792+ } ,
793+ {
794+ role : "user" ,
795+ content : [
796+ { type : "tool_result" , callId : "wf" , content : [ { type : "text" , text : "wrote" } ] } ,
797+ ] ,
798+ timestamp : 5 ,
799+ } ,
800+ {
801+ role : "user" ,
802+ content : [ { type : "text" , text : "keep" } ] ,
803+ timestamp : 6 ,
804+ } ,
805+ ] ;
806+
807+ const blocked = await wrapped . apply ( turns , ctx ) ;
808+ expect ( blocked . output ) . toBe ( turns ) ;
809+ expect ( blocked . record . reason ) . toBe ( "incomplete-evidence-archive" ) ;
810+
738811 await archive . recordAuthorizedPayload ( {
739- kind : "attachment " ,
740- payload : png ,
812+ kind : "user_message " ,
813+ payload : "do work" ,
741814 } ) ;
742815 await archive . recordAuthorizedPayload ( {
743- kind : "user_message" ,
744- payload : "keep" ,
816+ kind : "tool_args" ,
817+ payload : { name : "list_dir" , arguments : { path : "." } } ,
818+ callId : "ld" ,
819+ } ) ;
820+ await archive . recordAuthorizedPayload ( {
821+ kind : "tool_result" ,
822+ payload : "src/\n" ,
823+ callId : "ld" ,
824+ } ) ;
825+ await archive . recordAuthorizedPayload ( {
826+ kind : "tool_args" ,
827+ payload : { name : "write_file" , arguments : { path : "a.ts" } } ,
828+ callId : "wf" ,
829+ } ) ;
830+ await archive . recordAuthorizedPayload ( {
831+ kind : "tool_result" ,
832+ payload : "wrote" ,
833+ callId : "wf" ,
745834 } ) ;
835+
746836 const allowed = await wrapped . apply ( turns , ctx ) ;
747837 expect ( allowed . output ) . toHaveLength ( 1 ) ;
748838 expect ( allowed . record . reason ) . toBe ( "compact" ) ;
749839 } ) ;
840+
841+ test ( "cloned keep-window turns are not treated as dropped" , async ( ) => {
842+ const { wrapCompactorWithCompletenessGate } = await import ( "./compaction-archive.js" ) ;
843+ const { archive } = memoryArchive ( ) ;
844+ await archive . recordAuthorizedPayload ( {
845+ kind : "user_message" ,
846+ payload : "dropped-prefix" ,
847+ } ) ;
848+ const inner : import ( "@intx/types/runtime" ) . Compactor = {
849+ name : "pruning-compactor" ,
850+ version : "1" ,
851+ async apply ( turns ) {
852+ const kept = turns . slice ( - 1 ) . map ( ( turn ) => ( { ...turn , content : [ ...turn . content ] } ) ) ;
853+ return {
854+ output : kept ,
855+ record : {
856+ strategy : "pruning-compactor" ,
857+ version : "1" ,
858+ parameters : { } ,
859+ reason : "compact" ,
860+ decisions : { dropped : turns . length - 1 } ,
861+ } ,
862+ } ;
863+ } ,
864+ } ;
865+ const wrapped = wrapCompactorWithCompletenessGate ( inner , archive ) ;
866+ const turns : import ( "@intx/types/runtime" ) . ConversationTurn [ ] = [
867+ {
868+ role : "user" ,
869+ content : [ { type : "text" , text : "dropped-prefix" } ] ,
870+ timestamp : 1 ,
871+ } ,
872+ {
873+ role : "user" ,
874+ content : [ { type : "text" , text : "keep-window" } ] ,
875+ timestamp : 2 ,
876+ } ,
877+ ] ;
878+ const result = await wrapped . apply ( turns , ctx ) ;
879+ expect ( result . record . reason ) . toBe ( "compact" ) ;
880+ expect ( result . output ) . toHaveLength ( 1 ) ;
881+ expect ( result . output [ 0 ] ) . not . toBe ( turns [ 1 ] ) ;
882+ } ) ;
750883} ) ;
0 commit comments