@@ -527,7 +527,7 @@ export async function createSessionStores(
527527 const pendingBlobFilepaths = new Set < string > ( ) ;
528528 const pendingSegmentPaths = new Set < string > ( ) ;
529529 let writeTurnsSegmented = createSegmentedJSONLWriter ( dir , TURNS_FILE ) ;
530- const writePromptSegmented = createSegmentedJSONLWriter ( dir , PROMPT_FILE ) ;
530+ let writePromptSegmented = createSegmentedJSONLWriter ( dir , PROMPT_FILE ) ;
531531 let liveTurnRefs : readonly ConversationTurn [ ] | null = null ;
532532 let unpublishedRewrite : ConversationTurn [ ] | null = null ;
533533
@@ -606,6 +606,75 @@ export async function createSessionStores(
606606 liveTurnRefs = [ ...turns ] ;
607607 }
608608
609+ function promptEqualsLiveTurns (
610+ live : readonly ConversationTurn [ ] ,
611+ prompt : readonly ConversationTurn [ ] ,
612+ ) : boolean {
613+ if ( live . length !== prompt . length ) return false ;
614+ for ( let index = 0 ; index < prompt . length ; index ++ ) {
615+ if ( live [ index ] === prompt [ index ] ) continue ;
616+ if ( JSON . stringify ( live [ index ] ) !== JSON . stringify ( prompt [ index ] ) )
617+ return false ;
618+ }
619+ return true ;
620+ }
621+
622+ // The reactor checkpoints the materialized prompt every cycle, but most
623+ // cycles run no transform that changes it — writing an identical snapshot
624+ // next to turns.jsonl doubles disk and re-hash cost for zero information.
625+ // load() never reads prompt.jsonl (base turns + TURNS_FILE extras only),
626+ // so skipping the write leaves resume behavior unchanged; prompts that
627+ // actually differ still write exactly as before.
628+ async function writePromptIfDiffered (
629+ turns : readonly ConversationTurn [ ] ,
630+ ) : Promise < void > {
631+ let live : readonly ConversationTurn [ ] | null = null ;
632+ if ( unpublishedRewrite !== null ) live = unpublishedRewrite ;
633+ else if ( liveTurnRefs !== null ) live = liveTurnRefs ;
634+ else {
635+ // Fresh instance with no writeTurns yet: recover live turns from disk
636+ // the same way writeTurnsLiveOrStage does. Any failure falls through
637+ // to a normal write — an unreadable baseline must not drop the snapshot.
638+ try {
639+ const extraTexts = await readExtraSegmentTexts ( dir , TURNS_FILE ) ;
640+ let baseTurns : ConversationTurn [ ] ;
641+ try {
642+ baseTurns = ( await base . load ( ) ) . turns ;
643+ } catch {
644+ baseTurns = await readBaseTurnsFromDisk ( dir ) ;
645+ }
646+ live =
647+ extraTexts . length === 0
648+ ? baseTurns
649+ : await loadTurnsWithoutMalformedToolSequence (
650+ baseTurns ,
651+ extraTexts ,
652+ ) ;
653+ } catch {
654+ live = null ;
655+ }
656+ }
657+ if ( live === null || ! promptEqualsLiveTurns ( live , turns ) ) {
658+ await writeSegmented ( writePromptSegmented , turns ) ;
659+ return ;
660+ }
661+ // Identical to live turns: converge disk to no prompt segment so a stale
662+ // snapshot from an earlier differing write cannot linger. Removals join
663+ // pendingSegmentPaths so commit stages them out of the tree.
664+ const highest = await highestSegmentIndex ( dir , PROMPT_FILE ) ;
665+ let removed = false ;
666+ for ( let index = 0 ; index <= highest ; index ++ ) {
667+ const name = segmentFileName ( PROMPT_FILE , index ) ;
668+ if ( ! ( await pathExists ( path . join ( dir , name ) ) ) ) continue ;
669+ await fs . promises . unlink ( path . join ( dir , name ) ) ;
670+ pendingSegmentPaths . add ( name ) ;
671+ removed = true ;
672+ }
673+ if ( removed ) {
674+ writePromptSegmented = createSegmentedJSONLWriter ( dir , PROMPT_FILE ) ;
675+ }
676+ }
677+
609678 // Prefer the longest prefix of base + extras whose tool sequence the reactor
610679 // will accept. Orphan tails left by a fresh-writer compaction rewrite are
611680 // dropped and unlinked so the next load does not re-poison the session.
@@ -718,7 +787,9 @@ export async function createSessionStores(
718787 return [ ...baseTurns , ...parsedExtras . slice ( 0 , keepExtras ) . flat ( ) ] ;
719788 } ,
720789 readBlob : ( key , signal ) => base . readBlob ( key , signal ) ,
721- writePrompt : ( turns ) => writeSegmented ( writePromptSegmented , turns ) ,
790+ // Skipped identical snapshots fall back to live turns in load(), which
791+ // never reads prompt.jsonl.
792+ writePrompt : ( turns ) => writePromptIfDiffered ( turns ) ,
722793 writeResponse : ( turn , signal ) => base . writeResponse ( turn , signal ) ,
723794 writeManifest : ( records , signal ) => base . writeManifest ( records , signal ) ,
724795 writeTurns : ( turns ) => writeTurnsLiveOrStage ( turns ) ,
0 commit comments