@@ -26,6 +26,7 @@ const RESPONSE_FILE = "response.jsonl";
2626const MANIFEST_FILE = "manifest.jsonl" ;
2727const METADATA_FILE = "metadata.json" ;
2828const TOOL_OUTPUT_DIR = "tool-output" ;
29+ const EVIDENCE_ARCHIVE_DIR = "evidence-archive" ;
2930
3031const log = getLogger ( [ LOG_NAMESPACE_ROOT , "session" , "context-store" ] ) ;
3132
@@ -447,6 +448,30 @@ export async function createOptimizedContextStore(
447448 const pendingSegmentPaths = new Set < string > ( ) ;
448449 const writeTurnsSegmented = createSegmentedJSONLWriter ( dir , TURNS_FILE ) ;
449450 const writePromptSegmented = createSegmentedJSONLWriter ( dir , PROMPT_FILE ) ;
451+ let liveTurnRefs : readonly ConversationTurn [ ] | null = null ;
452+ let unpublishedRewrite : ConversationTurn [ ] | null = null ;
453+
454+ function refPrefixLength (
455+ prev : readonly ConversationTurn [ ] ,
456+ next : readonly ConversationTurn [ ] ,
457+ ) : number {
458+ const max = Math . min ( prev . length , next . length ) ;
459+ let prefix = 0 ;
460+ while ( prefix < max && prev [ prefix ] === next [ prefix ] ) prefix ++ ;
461+ return prefix ;
462+ }
463+
464+ function contentPrefixLength (
465+ prev : readonly ConversationTurn [ ] ,
466+ next : readonly ConversationTurn [ ] ,
467+ ) : number {
468+ const max = Math . min ( prev . length , next . length ) ;
469+ let prefix = 0 ;
470+ while ( prefix < max && JSON . stringify ( prev [ prefix ] ) === JSON . stringify ( next [ prefix ] ) ) {
471+ prefix ++ ;
472+ }
473+ return prefix ;
474+ }
450475
451476 async function writeSegmented (
452477 writer : ReturnType < typeof createSegmentedJSONLWriter > ,
@@ -488,6 +513,70 @@ export async function createOptimizedContextStore(
488513 return [ ...baseTurns , ...parsedExtras . slice ( 0 , keepExtras ) . flat ( ) ] ;
489514 }
490515
516+ async function loadLive ( signal ?: AbortSignal ) {
517+ try {
518+ const baseResult = await base . load ( signal ) ;
519+ const extraTexts = await readExtraSegmentTexts ( dir , TURNS_FILE ) ;
520+ if ( extraTexts . length === 0 ) return baseResult ;
521+ const turns = await loadTurnsWithoutMalformedToolSequence ( baseResult . turns , extraTexts ) ;
522+ return { ...baseResult , turns } ;
523+ } catch ( cause ) {
524+ log . warn ( "base context store load failed; recovering turns from disk segments" , {
525+ cause : cause instanceof Error ? cause . message : String ( cause ) ,
526+ } ) ;
527+ let baseTurns : ConversationTurn [ ] ;
528+ try {
529+ // Prefer resilient parse of segment 0 alone so orphan-tail heal still runs.
530+ // skipMalformed: mid-file garbage/interleaved records must not kill resume
531+ // (CL-7052); null-pad stripping and torn-tail drop still apply.
532+ const basePath = path . join ( dir , TURNS_FILE ) ;
533+ if ( await pathExists ( basePath ) ) {
534+ const text = await fs . promises . readFile ( basePath , "utf-8" ) ;
535+ baseTurns = parseSegmentTurns ( text , true , TURNS_FILE , true ) ;
536+ } else {
537+ baseTurns = [ ] ;
538+ }
539+ } catch ( parseCause ) {
540+ throw new Error (
541+ `failed to load ${ TURNS_FILE } : ${
542+ parseCause instanceof Error ? parseCause . message : String ( parseCause )
543+ } `,
544+ { cause : parseCause } ,
545+ ) ;
546+ }
547+ const extraTexts = await readExtraSegmentTexts ( dir , TURNS_FILE ) ;
548+ const turns =
549+ extraTexts . length === 0
550+ ? baseTurns
551+ : await loadTurnsWithoutMalformedToolSequence ( baseTurns , extraTexts ) ;
552+ const metadata = await loadMetadataSoft ( ( ) => base . loadMetadata ( ) ) ;
553+ return { turns, ...metadata } ;
554+ }
555+ }
556+
557+ async function writeTurnsLiveOrStage ( turns : readonly ConversationTurn [ ] ) : Promise < void > {
558+ if ( unpublishedRewrite !== null ) {
559+ unpublishedRewrite = [ ...turns ] ;
560+ return ;
561+ }
562+ if ( liveTurnRefs !== null ) {
563+ if ( refPrefixLength ( liveTurnRefs , turns ) < liveTurnRefs . length ) {
564+ unpublishedRewrite = [ ...turns ] ;
565+ return ;
566+ }
567+ await writeSegmented ( writeTurnsSegmented , turns ) ;
568+ liveTurnRefs = [ ...turns ] ;
569+ return ;
570+ }
571+ const live = ( await loadLive ( ) ) . turns ;
572+ if ( live . length > 0 && contentPrefixLength ( live , turns ) < live . length ) {
573+ unpublishedRewrite = [ ...turns ] ;
574+ return ;
575+ }
576+ await writeSegmented ( writeTurnsSegmented , turns ) ;
577+ liveTurnRefs = [ ...turns ] ;
578+ }
579+
491580 return {
492581 // Full-history read. Called by the reactor during initialization, where
493582 // the complete turn history is the actual live conversation state, not an
@@ -498,47 +587,7 @@ export async function createOptimizedContextStore(
498587 // garbage turns.jsonl), recover usable turns via resilient segment parse
499588 // and re-read metadata via the base schema (soft-empty only if that fails
500589 // too) so resume does not die on a bare Bun JSON token or wipe pending ops.
501- async load ( signal ) {
502- try {
503- const baseResult = await base . load ( signal ) ;
504- const extraTexts = await readExtraSegmentTexts ( dir , TURNS_FILE ) ;
505- if ( extraTexts . length === 0 ) return baseResult ;
506- const turns = await loadTurnsWithoutMalformedToolSequence ( baseResult . turns , extraTexts ) ;
507- return { ...baseResult , turns } ;
508- } catch ( cause ) {
509- log . warn ( "base context store load failed; recovering turns from disk segments" , {
510- cause : cause instanceof Error ? cause . message : String ( cause ) ,
511- } ) ;
512- let baseTurns : ConversationTurn [ ] ;
513- try {
514- // Prefer resilient parse of segment 0 alone so orphan-tail heal still runs.
515- // skipMalformed: mid-file garbage/interleaved records must not kill resume
516- // (CL-7052); null-pad stripping and torn-tail drop still apply.
517- const basePath = path . join ( dir , TURNS_FILE ) ;
518- if ( await pathExists ( basePath ) ) {
519- const text = await fs . promises . readFile ( basePath , "utf-8" ) ;
520- baseTurns = parseSegmentTurns ( text , true , TURNS_FILE , true ) ;
521- } else {
522- baseTurns = [ ] ;
523- }
524- } catch ( parseCause ) {
525- // Unrecoverable: rethrow with the file name in the message.
526- throw new Error (
527- `failed to load ${ TURNS_FILE } : ${
528- parseCause instanceof Error ? parseCause . message : String ( parseCause )
529- } `,
530- { cause : parseCause } ,
531- ) ;
532- }
533- const extraTexts = await readExtraSegmentTexts ( dir , TURNS_FILE ) ;
534- const turns =
535- extraTexts . length === 0
536- ? baseTurns
537- : await loadTurnsWithoutMalformedToolSequence ( baseTurns , extraTexts ) ;
538- const metadata = await loadMetadataSoft ( ( ) => base . loadMetadata ( ) ) ;
539- return { turns, ...metadata } ;
540- }
541- } ,
590+ load : ( signal ) => loadLive ( signal ) ,
542591 setConnectorState : ( state ) => base . setConnectorState ( state ) ,
543592 branch : ( name , signal ) => base . branch ( name , signal ) ,
544593 log : ( limit , signal ) => base . log ( limit , signal ) ,
@@ -562,7 +611,7 @@ export async function createOptimizedContextStore(
562611 writePrompt : ( turns ) => writeSegmented ( writePromptSegmented , turns ) ,
563612 writeResponse : ( turn , signal ) => base . writeResponse ( turn , signal ) ,
564613 writeManifest : ( records , signal ) => base . writeManifest ( records , signal ) ,
565- writeTurns : ( turns ) => writeSegmented ( writeTurnsSegmented , turns ) ,
614+ writeTurns : ( turns ) => writeTurnsLiveOrStage ( turns ) ,
566615 writeMetadata : ( metadata , signal ) => base . writeMetadata ( metadata , signal ) ,
567616 readManifestHistory : ( limit , signal ) => base . readManifestHistory ( limit , signal ) ,
568617 async writeBlob ( key , bytes , contentType , signal ) {
@@ -571,6 +620,12 @@ export async function createOptimizedContextStore(
571620 pendingBlobFilepaths . add ( `${ TOOL_OUTPUT_DIR } /${ filename } ` ) ;
572621 } ,
573622 async commit ( options , _signal ) {
623+ if ( unpublishedRewrite !== null ) {
624+ await writeSegmented ( writeTurnsSegmented , unpublishedRewrite ) ;
625+ liveTurnRefs = unpublishedRewrite ;
626+ unpublishedRewrite = null ;
627+ }
628+
574629 const toAdd : string [ ] = [ ] ;
575630 const toRemove : string [ ] = [ ] ;
576631
@@ -584,6 +639,10 @@ export async function createOptimizedContextStore(
584639 else toRemove . push ( filepath ) ;
585640 }
586641
642+ if ( await pathExists ( path . join ( dir , EVIDENCE_ARCHIVE_DIR ) ) ) {
643+ toAdd . push ( EVIDENCE_ARCHIVE_DIR ) ;
644+ }
645+
587646 // Disk is source of truth for which turn/prompt segments should remain
588647 // tracked after a rewrite or heal, even if pendingSegmentPaths was lost.
589648 await reconcileSegmentStaging ( dir , TURNS_FILE , toAdd , toRemove ) ;
0 commit comments