@@ -46,6 +46,7 @@ const METADATA_FILE = "metadata.json";
4646const TOKEN_USAGE_SIDECAR_FILE = "token-usage.jsonl" ;
4747const TOOL_OUTPUT_DIR = "tool-output" ;
4848const EVIDENCE_ARCHIVE_DIR = "evidence-archive" ;
49+ const EVIDENCE_INDEX_FILE = `${ EVIDENCE_ARCHIVE_DIR } /index.jsonl` ;
4950const OWNER_MARKER_FILE = "session-owner.json" ;
5051
5152const log = getLogger ( [ LOG_NAMESPACE_ROOT , "session" , "context-store" ] ) ;
@@ -1489,9 +1490,13 @@ export async function createSessionStores(
14891490 // Fingerprints from the last publish. Segment stats avoid re-reading an
14901491 // unchanged segment. Tool-output blobs are remembered per file: a directory
14911492 // mtime does not move when an existing blob is rewritten in place, and
1492- // statusMatrix is not asked to walk that directory on a no-op.
1493+ // statusMatrix is not asked to walk that directory on a no-op. The evidence
1494+ // index is one file: an in-place append does not move the archive directory,
1495+ // so it is fingerprinted on its own and is not a reason to walk tool-output.
14931496 let publishedSegmentStats = new Map < string , SegmentStat > ( ) ;
14941497 let publishedToolBlobs = new Map < string , SegmentStat > ( ) ;
1498+ let publishedEvidenceIndex : SegmentStat | undefined ;
1499+ let evidenceIndexObserved = false ;
14951500 let blobDirsObserved = false ;
14961501 let toolOutputFingerprint : DirectoryFingerprint | null = null ;
14971502 let evidenceFingerprint : DirectoryFingerprint | null = null ;
@@ -1575,6 +1580,53 @@ export async function createSessionStores(
15751580 return drifted ;
15761581 }
15771582
1583+ async function statEvidenceIndex ( ) : Promise < SegmentStat | undefined > {
1584+ try {
1585+ const fileStats = await fs . promises . lstat (
1586+ path . join ( dir , EVIDENCE_INDEX_FILE ) ,
1587+ ) ;
1588+ if ( ! fileStats . isFile ( ) ) return undefined ;
1589+ return {
1590+ bytes : fileStats . size ,
1591+ mtimeMs : fileStats . mtimeMs ,
1592+ ino : fileStats . ino ,
1593+ } ;
1594+ } catch ( cause ) {
1595+ if (
1596+ cause instanceof Error &&
1597+ "code" in cause &&
1598+ cause . code === "ENOENT"
1599+ ) {
1600+ return undefined ;
1601+ }
1602+ throw cause ;
1603+ }
1604+ }
1605+
1606+ // In-place appends do not move the archive directory. Compare the one index
1607+ // file saved at the last publish. Absent before and after is not drift, and
1608+ // a match is not read. A store that has not published yet leaves this to the
1609+ // directory walk.
1610+ async function driftedPublishedEvidenceIndex ( ) : Promise < {
1611+ filepath : string ;
1612+ present : boolean ;
1613+ } | null > {
1614+ if ( ! evidenceIndexObserved ) return null ;
1615+ const observed = await statEvidenceIndex ( ) ;
1616+ if ( observed === undefined ) {
1617+ return publishedEvidenceIndex === undefined
1618+ ? null
1619+ : { filepath : EVIDENCE_INDEX_FILE , present : false } ;
1620+ }
1621+ if (
1622+ publishedEvidenceIndex === undefined ||
1623+ segmentStatDrifted ( observed , publishedEvidenceIndex )
1624+ ) {
1625+ return { filepath : EVIDENCE_INDEX_FILE , present : true } ;
1626+ }
1627+ return null ;
1628+ }
1629+
15781630 async function blobDirectoriesNeedWalk ( ) : Promise < boolean > {
15791631 if ( ! blobDirsObserved ) return true ;
15801632 const tool = await directoryFingerprint ( dir , TOOL_OUTPUT_DIR ) ;
@@ -1612,6 +1664,8 @@ export async function createSessionStores(
16121664 toolOutputFingerprint = tool ;
16131665 evidenceFingerprint = evidence ;
16141666 blobDirsObserved = true ;
1667+ publishedEvidenceIndex = await statEvidenceIndex ( ) ;
1668+ evidenceIndexObserved = true ;
16151669 }
16161670 const ownerToken = opts ?. ownerId ?? globalThis . crypto . randomUUID ( ) ;
16171671 const explicitOwnerId = opts ?. ownerId ;
@@ -2466,7 +2520,10 @@ export async function createSessionStores(
24662520 // mtime or inode, so the directory walk stays off. Stat only the
24672521 // blobs recorded at the last publish, and only when nothing else
24682522 // is already forcing a commit (that path git.adds the directory).
2523+ // The evidence index is the same kind of in-place append, and it
2524+ // is one file: stat it, do not walk tool-output to notice it.
24692525 let toolBlobsDrifted = false ;
2526+ let evidenceIndexDrifted = false ;
24702527 if ( ! rootsDiffer ) {
24712528 for ( const blob of await driftedPublishedToolBlobs ( ) ) {
24722529 toolBlobsDrifted = true ;
@@ -2475,8 +2532,14 @@ export async function createSessionStores(
24752532 restatBlobs . add ( blob . filepath ) ;
24762533 } else toRemove . push ( blob . filepath ) ;
24772534 }
2535+ const index = await driftedPublishedEvidenceIndex ( ) ;
2536+ if ( index !== null ) {
2537+ evidenceIndexDrifted = true ;
2538+ if ( index . present ) toAdd . push ( index . filepath ) ;
2539+ else toRemove . push ( index . filepath ) ;
2540+ }
24782541 }
2479- if ( ! rootsDiffer && ! toolBlobsDrifted ) {
2542+ if ( ! rootsDiffer && ! toolBlobsDrifted && ! evidenceIndexDrifted ) {
24802543 const [ head ] = await base . log ( 1 ) ;
24812544 if ( head !== undefined ) {
24822545 pendingBlobFilepaths . clear ( ) ;
0 commit comments