@@ -60,6 +60,7 @@ function resolveMaxToolResultChars(opts?: { maxToolResultChars?: number }): numb
6060
6161type UserAgentMessage = Extract < AgentMessage , { role : "user" } > ;
6262type CompactionAppendValidator = ( entryId : string , appendedText : string ) => boolean ;
63+ type AppendMessageOptions = Parameters < SessionManager [ "appendMessage" ] > [ 1 ] ;
6364
6465function isUserAgentMessage ( message : AgentMessage ) : message is UserAgentMessage {
6566 return message . role === "user" ;
@@ -643,6 +644,7 @@ export function installSessionToolResultGuard(
643644 const allowSyntheticToolResults = opts ?. allowSyntheticToolResults ?? true ;
644645 const missingToolResultText = opts ?. missingToolResultText ;
645646 const beforeWrite = opts ?. beforeMessageWriteHook ;
647+ const toolResultTransformerMayMutate = opts ?. transformToolResultForPersistence !== undefined ;
646648 const redactionConfig = opts ?. redactLoggingConfig ;
647649 const maxToolResultChars = resolveMaxToolResultChars ( opts ) ;
648650 const transcriptSeqByEntryId : TranscriptSeqByEntryId = new Map ( ) ;
@@ -653,9 +655,10 @@ export function installSessionToolResultGuard(
653655
654656 const appendMessageAndCacheTranscriptSeq = (
655657 message : AgentMessage ,
658+ options ?: AppendMessageOptions ,
656659 ) : { entryId : string ; messageSeq ?: number ; sessionFile ?: string | null } => {
657660 const parentEntryId = sessionManager . getLeafId ( ) ;
658- const entryId = originalAppend ( message as never ) ;
661+ const entryId = originalAppend ( message as never , options ) ;
659662 void opts ?. onMessagePersisted ?.( message ) ;
660663 const sessionFile = getSessionFile ( ) ;
661664 if ( ! sessionFile ) {
@@ -686,18 +689,20 @@ export function installSessionToolResultGuard(
686689 * Run the before_message_write hook. Returns the (possibly modified) message,
687690 * or null if the message should be blocked.
688691 */
689- const applyBeforeWriteHook = ( msg : AgentMessage ) : AgentMessage | null => {
692+ const applyBeforeWriteHook = (
693+ msg : AgentMessage ,
694+ ) : { message : AgentMessage ; changed : boolean } | null => {
690695 if ( ! beforeWrite ) {
691- return msg ;
696+ return { message : msg , changed : false } ;
692697 }
693698 const result = beforeWrite ( { message : msg } ) ;
694699 if ( result ?. block ) {
695700 return null ;
696701 }
697702 if ( result ?. message ) {
698- return result . message ;
703+ return { message : result . message , changed : true } ;
699704 }
700- return msg ;
705+ return { message : msg , changed : false } ;
701706 } ;
702707
703708 const flushPendingToolResults = ( ) => {
@@ -711,16 +716,20 @@ export function installSessionToolResultGuard(
711716 toolName : name ,
712717 text : missingToolResultText ,
713718 } ) ;
714- const flushed = applyBeforeWriteHook (
715- persistToolResult ( persistMessage ( synthetic ) , {
716- toolCallId : id ,
717- toolName : name ,
718- isSynthetic : true ,
719- } ) ,
720- ) ;
719+ const persistedSynthetic = persistMessage ( synthetic ) ;
720+ const transformed = persistToolResult ( persistedSynthetic , {
721+ toolCallId : id ,
722+ toolName : name ,
723+ isSynthetic : true ,
724+ } ) ;
725+ const flushed = applyBeforeWriteHook ( transformed ) ;
721726 if ( flushed ) {
722727 appendMessageAndCacheTranscriptSeq (
723- capToolResultForPersistence ( flushed , maxToolResultChars , redactionConfig ) ,
728+ capToolResultForPersistence ( flushed . message , maxToolResultChars , redactionConfig ) ,
729+ {
730+ invalidateSerializedPrefixCache :
731+ persistedSynthetic !== synthetic || toolResultTransformerMayMutate || flushed . changed ,
732+ } ,
724733 ) ;
725734 }
726735 }
@@ -732,7 +741,8 @@ export function installSessionToolResultGuard(
732741 pendingState . clear ( ) ;
733742 } ;
734743
735- const guardedAppend = ( message : AgentMessage ) => {
744+ const guardedAppend = ( message : AgentMessage , callerOptions ?: AppendMessageOptions ) => {
745+ const callerInvalidatesCache = callerOptions ?. invalidateSerializedPrefixCache === true ;
736746 let nextMessage = message ;
737747 const role = ( message as { role ?: unknown } ) . role ;
738748 if ( role === "assistant" ) {
@@ -758,23 +768,30 @@ export function installSessionToolResultGuard(
758768 const normalizedToolResult = normalizePersistedToolResultName ( nextMessage , toolName ) ;
759769 // Apply hard size cap before persistence to prevent oversized tool results
760770 // from consuming the entire context window on subsequent LLM calls.
771+ const persistedToolResult = persistMessage ( normalizedToolResult ) ;
761772 const capped = capToolResultForPersistence (
762- persistMessage ( normalizedToolResult ) ,
773+ persistedToolResult ,
763774 maxToolResultChars ,
764775 redactionConfig ,
765776 ) ;
766- const persisted = applyBeforeWriteHook (
767- persistToolResult ( capped , {
768- toolCallId : id ?? undefined ,
769- toolName,
770- isSynthetic : false ,
771- } ) ,
772- ) ;
777+ const transformed = persistToolResult ( capped , {
778+ toolCallId : id ?? undefined ,
779+ toolName,
780+ isSynthetic : false ,
781+ } ) ;
782+ const persisted = applyBeforeWriteHook ( transformed ) ;
773783 if ( ! persisted ) {
774784 return undefined ;
775785 }
776786 return appendMessageAndCacheTranscriptSeq (
777- capToolResultForPersistence ( persisted , maxToolResultChars , redactionConfig ) ,
787+ capToolResultForPersistence ( persisted . message , maxToolResultChars , redactionConfig ) ,
788+ {
789+ invalidateSerializedPrefixCache :
790+ callerInvalidatesCache ||
791+ persistedToolResult !== normalizedToolResult ||
792+ toolResultTransformerMayMutate ||
793+ persisted . changed ,
794+ } ,
778795 ) . entryId ;
779796 }
780797
@@ -818,10 +835,12 @@ export function installSessionToolResultGuard(
818835 flushPendingToolResults ( ) ;
819836 }
820837
821- const finalMessage = applyBeforeWriteHook ( persistMessage ( nextMessage ) ) ;
822- if ( ! finalMessage ) {
838+ const transformedMessage = persistMessage ( nextMessage ) ;
839+ const finalWrite = applyBeforeWriteHook ( transformedMessage ) ;
840+ if ( ! finalWrite ) {
823841 return undefined ;
824842 }
843+ const finalMessage = finalWrite . message ;
825844 const finalRole = ( finalMessage as { role ?: unknown } ) . role ;
826845 if (
827846 finalRole === "assistant" &&
@@ -845,7 +864,10 @@ export function installSessionToolResultGuard(
845864 entryId : result ,
846865 messageSeq,
847866 sessionFile,
848- } = appendMessageAndCacheTranscriptSeq ( finalMessage ) ;
867+ } = appendMessageAndCacheTranscriptSeq ( finalMessage , {
868+ invalidateSerializedPrefixCache :
869+ callerInvalidatesCache || transformedMessage !== nextMessage || finalWrite . changed ,
870+ } ) ;
849871 if ( sessionFile ) {
850872 emitSessionTranscriptUpdate ( {
851873 sessionFile,
0 commit comments