@@ -742,6 +742,107 @@ describe("createOptimizedContextStore checkpoint", () => {
742742 expect ( new TextDecoder ( ) . decode ( blob ) ) . toContain ( marker ) ;
743743 } ) ;
744744
745+ test ( "a same-length rewrite of turns-0001.jsonl inside the same mtime second is in the next commit" , async ( ) => {
746+ const dir = tempDir ( ) ;
747+ const store = await createOptimizedContextStore ( dir ) ;
748+ const chunk = "s" . repeat ( 100_000 ) ;
749+ const turns : ConversationTurn [ ] = [ ] ;
750+ const closed = segmentFileName ( TURNS_FILE , 1 ) ;
751+ const tail = segmentFileName ( TURNS_FILE , 2 ) ;
752+ do {
753+ turns . push ( turn ( `${ turns . length } -${ chunk } ` ) ) ;
754+ await store . writeTurns ( turns ) ;
755+ } while ( ! fs . existsSync ( path . join ( dir , tail ) ) && turns . length < 20 ) ;
756+ expect ( fs . existsSync ( path . join ( dir , closed ) ) ) . toBe ( true ) ;
757+ await store . writeMetadata ( EMPTY_CHECKPOINT_METADATA ) ;
758+ const first = await store . commit ( { message : "three segments" } ) ;
759+
760+ const closedPath = path . join ( dir , closed ) ;
761+ const before = fs . statSync ( closedPath ) ;
762+ const marker = "same-second-rewrite-marker" ;
763+ const original = fs . readFileSync ( closedPath , "utf8" ) ;
764+ const at = original . indexOf ( "s" . repeat ( marker . length ) ) ;
765+ expect ( at ) . toBeGreaterThanOrEqual ( 0 ) ;
766+ const rewritten =
767+ original . slice ( 0 , at ) + marker + original . slice ( at + marker . length ) ;
768+ expect ( rewritten . length ) . toBe ( original . length ) ;
769+ fs . writeFileSync ( closedPath , rewritten ) ;
770+ const nudged = nudgedSameSecondMs ( before . mtimeMs ) ;
771+ fs . utimesSync ( closedPath , before . atime , new Date ( nudged ) ) ;
772+ const after = fs . statSync ( closedPath ) ;
773+ // The racy-git window: same inode, same size, same whole mtime second.
774+ // statusMatrix then trusts the index oid and misses the new bytes.
775+ expect ( after . ino ) . toBe ( before . ino ) ;
776+ expect ( after . size ) . toBe ( before . size ) ;
777+ expect ( Math . floor ( after . mtimeMs / 1000 ) ) . toBe (
778+ Math . floor ( before . mtimeMs / 1000 ) ,
779+ ) ;
780+ expect ( after . mtimeMs ) . not . toBe ( before . mtimeMs ) ;
781+
782+ let secondHash = "" ;
783+ await pinLstatCtime ( closedPath , before . ctimeMs , async ( ) => {
784+ const second = await store . commit ( {
785+ message : "publish same-second rewrite" ,
786+ } ) ;
787+ secondHash = second . hash ;
788+ } ) ;
789+ expect ( secondHash ) . not . toBe ( first . hash ) ;
790+ const published = await git . readBlob ( {
791+ fs,
792+ dir,
793+ oid : secondHash ,
794+ filepath : closed ,
795+ } ) ;
796+ expect ( new TextDecoder ( ) . decode ( published . blob ) ) . toContain ( marker ) ;
797+
798+ const third = await store . commit ( { message : "no-op after same-second" } ) ;
799+ expect ( third . hash ) . toBe ( secondHash ) ;
800+ const kept = await git . readBlob ( {
801+ fs,
802+ dir,
803+ oid : third . hash ,
804+ filepath : closed ,
805+ } ) ;
806+ expect ( new TextDecoder ( ) . decode ( kept . blob ) ) . toContain ( marker ) ;
807+ } ) ;
808+
809+ test ( "an in-place rewrite of an existing tool-output file is in the next commit" , async ( ) => {
810+ const dir = tempDir ( ) ;
811+ const store = await createOptimizedContextStore ( dir ) ;
812+ await store . writeBlob (
813+ "kept" ,
814+ new TextEncoder ( ) . encode ( "original" ) ,
815+ "text/plain" ,
816+ ) ;
817+ await store . writeTurns ( [ turn ( "seed" ) ] ) ;
818+ await store . writeMetadata ( EMPTY_CHECKPOINT_METADATA ) ;
819+ const first = await store . commit ( { message : "seed blob" } ) ;
820+
821+ const blobName = fs
822+ . readdirSync ( path . join ( dir , "tool-output" ) )
823+ . find ( ( name ) => name . startsWith ( "kept" ) ) ;
824+ if ( blobName === undefined ) throw new Error ( "seeded blob missing" ) ;
825+ const blobPath = path . join ( dir , "tool-output" , blobName ) ;
826+ const before = fs . statSync ( blobPath ) ;
827+ const dirBefore = fs . statSync ( path . join ( dir , "tool-output" ) ) ;
828+ fs . writeFileSync ( blobPath , "rewritten-in-place" ) ;
829+ const after = fs . statSync ( blobPath ) ;
830+ const dirAfter = fs . statSync ( path . join ( dir , "tool-output" ) ) ;
831+ expect ( after . ino ) . toBe ( before . ino ) ;
832+ expect ( dirAfter . mtimeMs ) . toBe ( dirBefore . mtimeMs ) ;
833+ expect ( dirAfter . ino ) . toBe ( dirBefore . ino ) ;
834+
835+ const second = await store . commit ( { message : "publish in-place blob" } ) ;
836+ expect ( second . hash ) . not . toBe ( first . hash ) ;
837+ const { blob } = await git . readBlob ( {
838+ fs,
839+ dir,
840+ oid : second . hash ,
841+ filepath : `tool-output/${ blobName } ` ,
842+ } ) ;
843+ expect ( new TextDecoder ( ) . decode ( blob ) ) . toBe ( "rewritten-in-place" ) ;
844+ } ) ;
845+
745846 test ( "a turn commit does not lstat every seeded tool-output blob" , async ( ) => {
746847 const dir = tempDir ( ) ;
747848 const store = await createOptimizedContextStore ( dir ) ;
@@ -788,12 +889,34 @@ describe("createOptimizedContextStore checkpoint", () => {
788889 await store . writeMetadata ( EMPTY_CHECKPOINT_METADATA ) ;
789890 await store . commit ( { message : "seed blobs" } ) ;
790891
791- const lstats = await countPathOps (
792- "lstat" ,
793- `${ path . sep } tool-output${ path . sep } ` ,
794- ( ) => store . commit ( { message : "no writes" } ) ,
795- ) ;
796- expect ( lstats ) . toBe ( 0 ) ;
892+ const walkedToolOutput : string [ ] [ ] = [ ] ;
893+ const originalStatus = git . statusMatrix . bind ( git ) ;
894+ git . statusMatrix = ( async ( args ) => {
895+ const filepaths = args . filepaths ?? [ ] ;
896+ if (
897+ filepaths . some (
898+ ( filepath ) =>
899+ filepath === "tool-output" || filepath . startsWith ( "tool-output/" ) ,
900+ )
901+ ) {
902+ walkedToolOutput . push ( [ ...filepaths ] ) ;
903+ }
904+ return originalStatus ( args ) ;
905+ } ) as typeof git . statusMatrix ;
906+ let lstats = 0 ;
907+ try {
908+ lstats = await countPathOps (
909+ "lstat" ,
910+ `${ path . sep } tool-output${ path . sep } ` ,
911+ ( ) => store . commit ( { message : "no writes" } ) ,
912+ ) ;
913+ } finally {
914+ git . statusMatrix = originalStatus ;
915+ }
916+ // Known blobs are statted by fingerprint. The directory itself is not
917+ // passed to statusMatrix, so children are not walked a second time.
918+ expect ( walkedToolOutput ) . toEqual ( [ ] ) ;
919+ expect ( lstats ) . toBe ( blobCount ) ;
797920 } ) ;
798921
799922 test ( "seeded tool-output does not commit a no-op and a fresh store still heals blob bytes" , async ( ) => {
@@ -1766,6 +1889,42 @@ function readTurnsFromFile(fullPath: string): ConversationTurn[] {
17661889 . map ( ( line ) => JSON . parse ( line ) as ConversationTurn ) ;
17671890}
17681891
1892+ function nudgedSameSecondMs ( mtimeMs : number ) : number {
1893+ const nudged = mtimeMs + 1 ;
1894+ if ( Math . floor ( nudged / 1000 ) === Math . floor ( mtimeMs / 1000 ) ) return nudged ;
1895+ return mtimeMs - 1 ;
1896+ }
1897+
1898+ /**
1899+ * isomorphic-git compareStats ignores sub-second mtime but still compares
1900+ * ctime seconds. A real rewrite bumps ctime to "now", which leaves the
1901+ * same-second window whenever the test crosses a second boundary. Report the
1902+ * pre-rewrite ctime for this one path so statusMatrix trusts the index oid.
1903+ */
1904+ async function pinLstatCtime < T > (
1905+ filePath : string ,
1906+ ctimeMs : number ,
1907+ run : ( ) => Promise < T > ,
1908+ ) : Promise < T > {
1909+ const original = fs . promises . lstat ;
1910+ fs . promises . lstat = ( async ( target : fs . PathLike , opts ?: fs . StatOptions ) => {
1911+ const stats = await original . call ( fs . promises , target , opts ) ;
1912+ if ( typeof target !== "string" || target !== filePath ) return stats ;
1913+ const pinned = Object . assign (
1914+ Object . create ( Object . getPrototypeOf ( stats ) ) ,
1915+ stats ,
1916+ ) as fs . Stats ;
1917+ pinned . ctimeMs = ctimeMs ;
1918+ pinned . ctime = new Date ( ctimeMs ) ;
1919+ return pinned ;
1920+ } ) as typeof fs . promises . lstat ;
1921+ try {
1922+ return await run ( ) ;
1923+ } finally {
1924+ fs . promises . lstat = original ;
1925+ }
1926+ }
1927+
17691928async function countPathOps (
17701929 op : "readFile" | "lstat" ,
17711930 marker : string ,
0 commit comments