@@ -133,7 +133,10 @@ describe('BlockExecutor', () => {
133133 expect ( context . mcpBlockId ) . toBeUndefined ( )
134134 } )
135135
136- function createFailedToolExecution ( output : Record < string , unknown > ) {
136+ function createFailedToolExecution (
137+ output : Record < string , unknown > ,
138+ options : { piiRedaction ?: boolean } = { }
139+ ) {
137140 const block = createBlock ( )
138141 const workflow : SerializedWorkflow = {
139142 version : '1' ,
@@ -159,16 +162,126 @@ describe('BlockExecutor', () => {
159162 state
160163 )
161164 const ctx = createContext ( state )
162- ctx . piiBlockOutputRedaction = {
163- enabled : true ,
164- entityTypes : [ 'EMAIL_ADDRESS' ] ,
165- language : 'en' ,
165+ if ( options . piiRedaction !== false ) {
166+ ctx . piiBlockOutputRedaction = {
167+ enabled : true ,
168+ entityTypes : [ 'EMAIL_ADDRESS' ] ,
169+ language : 'en' ,
170+ }
166171 }
167172 const node = createNode ( block )
168173 node . outgoingEdges . set ( 'error-edge' , { sourceHandle : EDGE . ERROR , target : 'error-handler' } )
169174 return { executor, block, state, ctx, node, failure, onBlockComplete }
170175 }
171176
177+ it ( 'durably compacts inline failed tool rows before error-port state and completion output' , async ( ) => {
178+ const rows = Array . from ( { length : 3072 } , ( _ , id ) => ( { id, value : 'p' . repeat ( 3072 ) } ) )
179+ const persisted = new Map < string , Buffer > ( )
180+ mockUploadFile . mockImplementation ( async ( { customKey, file } ) => {
181+ persisted . set ( customKey , file )
182+ return { key : customKey }
183+ } )
184+ mockDownloadFile . mockImplementation ( async ( { key } ) => {
185+ const file = persisted . get ( key )
186+ if ( ! file ) throw new Error ( 'Stored partial-row chunk was not found' )
187+ return file
188+ } )
189+ const { executor, block, state, ctx, node, failure, onBlockComplete } =
190+ createFailedToolExecution (
191+ { rows, rowCount : rows . length , incomplete : true } ,
192+ { piiRedaction : false }
193+ )
194+ const cost = { input : 0.1 , output : 0.2 , total : 0.3 }
195+ attachTrustedExecutionCost ( failure , cost )
196+
197+ const output = await executor . execute ( ctx , node , block )
198+ await vi . waitFor ( ( ) => expect ( onBlockComplete ) . toHaveBeenCalledOnce ( ) )
199+
200+ expect ( isLargeArrayManifest ( output . rows ) ) . toBe ( true )
201+ expect ( isLargeValueRef ( output ) ) . toBe ( false )
202+ expect ( output ) . toMatchObject ( {
203+ error : 'query incomplete' ,
204+ rowCount : rows . length ,
205+ incomplete : true ,
206+ cost,
207+ } )
208+ if ( ! isLargeArrayManifest ( output . rows ) ) throw new Error ( 'Expected compacted failed rows' )
209+ expect ( output . rows . totalCount ) . toBe ( rows . length )
210+ expect ( output . rows . chunks . every ( ( { ref } ) => ref . executionId === ctx . executionId ) ) . toBe ( true )
211+ clearLargeValueCacheForTests ( )
212+ expect (
213+ await readLargeArrayManifestSlice ( output . rows , 0 , rows . length , {
214+ workspaceId : ctx . workspaceId ,
215+ workflowId : ctx . workflowId ,
216+ executionId : ctx . executionId ,
217+ } )
218+ ) . toEqual ( rows )
219+ expect ( state . getBlockOutput ( block . id ) ) . toEqual ( output )
220+ expect ( ctx . blockLogs [ 0 ] ?. output ) . toEqual ( output )
221+ expect ( onBlockComplete . mock . calls [ 0 ] ?. [ 3 ] ?. output ) . toEqual ( output )
222+ expect ( ctx . blockLogs [ 0 ] ) . toMatchObject ( { success : false , errorHandled : true } )
223+ } )
224+
225+ it ( 'keeps aggregate failed envelopes addressable by error routing and named fields' , async ( ) => {
226+ const rows = Array . from ( { length : 1536 } , ( _ , id ) => ( { id, value : 'p' . repeat ( 3072 ) } ) )
227+ const metadata = { notes : 'm' . repeat ( 4.5 * 1024 * 1024 ) }
228+ const { executor, block, state, ctx, node, failure, onBlockComplete } =
229+ createFailedToolExecution (
230+ { rows, metadata, rowCount : rows . length , incomplete : true } ,
231+ { piiRedaction : false }
232+ )
233+ const cost = { input : 0.1 , output : 0.2 , total : 0.3 }
234+ attachTrustedExecutionCost ( failure , cost )
235+
236+ const output = await executor . execute ( ctx , node , block )
237+ await vi . waitFor ( ( ) => expect ( onBlockComplete ) . toHaveBeenCalledOnce ( ) )
238+
239+ expect ( isLargeValueRef ( output ) ) . toBe ( false )
240+ expect ( output . error ) . toBe ( 'query incomplete' )
241+ expect ( output . rowCount ) . toBe ( rows . length )
242+ expect ( output . incomplete ) . toBe ( true )
243+ expect ( output . cost ) . toEqual ( cost )
244+ expect ( JSON . stringify ( output . rows ) === JSON . stringify ( rows ) ) . toBe ( true )
245+ expect ( JSON . stringify ( output . metadata ) === JSON . stringify ( metadata ) ) . toBe ( true )
246+ expect ( state . getBlockOutput ( block . id ) ) . toEqual ( output )
247+ expect ( ctx . blockLogs [ 0 ] ?. output ) . toEqual ( output )
248+ expect ( onBlockComplete . mock . calls [ 0 ] ?. [ 3 ] ?. output ) . toEqual ( output )
249+ expect ( ctx . blockLogs [ 0 ] ) . toMatchObject ( { success : false , errorHandled : true } )
250+ } )
251+
252+ it ( 'omits retained tool payloads when durable compaction fails while preserving trusted cost' , async ( ) => {
253+ const rows = Array . from ( { length : 3072 } , ( _ , id ) => ( { id, value : 'p' . repeat ( 3072 ) } ) )
254+ const unsafeFailure = 'storage rejected private-partial-row-payload'
255+ mockUploadFile . mockRejectedValueOnce ( new Error ( unsafeFailure ) )
256+ const { executor, block, state, ctx, node, failure, onBlockComplete } =
257+ createFailedToolExecution (
258+ { rows, rowCount : rows . length , incomplete : true } ,
259+ { piiRedaction : false }
260+ )
261+ const cost = { input : 0.1 , output : 0.2 , total : 0.3 }
262+ attachTrustedExecutionCost ( failure , cost )
263+
264+ const output = await executor . execute ( ctx , node , block )
265+ await vi . waitFor ( ( ) => expect ( onBlockComplete ) . toHaveBeenCalledOnce ( ) )
266+
267+ expect ( Object . hasOwn ( output , 'rows' ) ) . toBe ( false )
268+ expect ( Object . hasOwn ( output , 'rowCount' ) ) . toBe ( false )
269+ expect ( Object . hasOwn ( output , 'incomplete' ) ) . toBe ( false )
270+ const expected = {
271+ error : 'Partial tool output could not be stored and was omitted.' ,
272+ cost,
273+ }
274+ expect ( output ) . toEqual ( expected )
275+ expect ( state . getBlockOutput ( block . id ) ) . toEqual ( expected )
276+ expect ( ctx . blockLogs [ 0 ] ?. output ) . toEqual ( expected )
277+ expect ( ctx . blockLogs [ 0 ] ?. error ) . toBe ( expected . error )
278+ expect ( onBlockComplete . mock . calls [ 0 ] ?. [ 3 ] ?. output ) . toEqual ( expected )
279+ expect ( ctx . blockLogs [ 0 ] ) . toMatchObject ( { success : false , errorHandled : true } )
280+ expect ( JSON . stringify ( [ output , ctx . blockLogs , onBlockComplete . mock . calls ] ) ) . not . toContain (
281+ unsafeFailure
282+ )
283+ } )
284+
172285 it ( 'masks partial failed tool rows before error-port state and completion output' , async ( ) => {
173286 mockMaskBatch . mockImplementation ( async ( texts : string [ ] ) =>
174287 texts . map ( ( text ) => text . replaceAll ( 'alice@example.com' , '<EMAIL_ADDRESS>' ) )
0 commit comments