11import { createHash } from 'node:crypto' ;
2- import { CanonicalHistoryJsonlDataSource } from '../packages/local-runtime-v2/src/infra/file/canonical-history-jsonl.js' ;
2+ import {
3+ canonicalActiveHistoryRevision ,
4+ canonicalHistoryRevision ,
5+ CanonicalHistoryJsonlDataSource ,
6+ decodeCanonicalHistoryEnvelope ,
7+ } from '../packages/local-runtime-v2/src/infra/file/canonical-history-jsonl.js' ;
8+ import { canonicalJson } from '../packages/local-runtime-v2/src/infra/file/canonical-history-json-value.js' ;
39import { mkdtemp , rm , writeFile } from 'node:fs/promises' ;
410import { tmpdir } from 'node:os' ;
511import { join } from 'node:path' ;
612import { describe , expect , it , vi } from 'vitest' ;
13+ import { eq } from 'drizzle-orm' ;
14+ import { DatabaseClient } from '../packages/local-runtime-v2/src/infra/db/client.js' ;
15+ import { initializeDatabase } from '../packages/local-runtime-v2/src/infra/db/initialize.js' ;
16+ import { queryCollapseViewStates } from '../packages/local-runtime-v2/src/infra/db/schema/query-collapse.js' ;
17+ import { turnIngress } from '../packages/local-runtime-v2/src/infra/db/schema/turn.js' ;
18+ import { createQueryCollapseState } from '../packages/local-runtime-v2/src/service/session-system/query-collapse-state.js' ;
19+ import { createQueueTurnAdmissionPriorityFence } from '../packages/local-runtime-v2/src/service/session-system/index.js' ;
20+ import { createTurnRepository } from '../packages/local-runtime-v2/src/service/turn-system/persistence/turn.repository.js' ;
721import { IncrementalSha256 } from '../packages/local-runtime-v2/src/service/turn-system/agent-host/history/incremental-sha256.js' ;
822import { captureSemanticSnapshot } from '../packages/local-runtime-v2/src/service/turn-system/agent-host/history/semantic-identity.js' ;
923import { DurableCanonicalHistoryStore } from '../packages/local-runtime-v2/src/service/turn-system/agent-host/history/durable-canonical-history-store.js' ;
@@ -18,6 +32,106 @@ import {
1832} from '../packages/local-runtime-v2/src/service/session-system/messages/history/session-history-paths.js' ;
1933import type { SessionRecord } from '../packages/local-runtime-v2/src/service/session-system/sessions/repo/contract.js' ;
2034
35+ describe ( 'prepared runtime reads' , ( ) => {
36+ async function withDatabase (
37+ run : ( client : DatabaseClient , writer : DatabaseClient ) => Promise < void > ,
38+ ) {
39+ const dataDir = await mkdtemp ( join ( tmpdir ( ) , 'mcode-prepared-reads-' ) ) ;
40+ const client = new DatabaseClient ( { dataDir } ) ;
41+ const writer = new DatabaseClient ( { dataDir } ) ;
42+ try {
43+ await initializeDatabase ( { database : client , dataDir } ) ;
44+ await run ( client , writer ) ;
45+ } finally {
46+ writer . close ( ) ;
47+ client . close ( ) ;
48+ await rm ( dataDir , { recursive : true , force : true } ) ;
49+ }
50+ }
51+
52+ it ( 'keeps processing reads fresh across sessions, completion and another connection' , async ( ) => {
53+ await withDatabase ( async ( client , writer ) => {
54+ const state = createQueryCollapseState ( { db : client . db , nowMs : ( ) => 1 } ) ;
55+ expect ( await state . findProcessingByCurrentTurn ( 's1' , 't1' ) ) . toBeUndefined ( ) ;
56+ await state . start ( { sessionId : 's1' , currentTurnId : 't1' , queryKey : 'a' } ) ;
57+ await state . start ( { sessionId : 's1' , currentTurnId : 't1' , queryKey : 'b' } ) ;
58+ await state . start ( { sessionId : 's2' , currentTurnId : 't1' , queryKey : 'other' } ) ;
59+ expect ( ( await state . findProcessingByCurrentTurn ( 's1' , 't1' ) ) ?. queryKey ) . toBe ( 'b' ) ;
60+ expect ( ( await state . findProcessingByCurrentTurn ( 's2' , 't1' ) ) ?. queryKey ) . toBe ( 'other' ) ;
61+ expect ( await state . findProcessingByCurrentTurn ( 's1' , 'missing' ) ) . toBeUndefined ( ) ;
62+ await state . finish ( {
63+ sessionId : 's1' ,
64+ currentTurnId : 't1' ,
65+ queryKey : 'b' ,
66+ forceExpanded : false ,
67+ } ) ;
68+ expect ( ( await state . findProcessingByCurrentTurn ( 's1' , 't1' ) ) ?. queryKey ) . toBe ( 'a' ) ;
69+ writer . db
70+ . update ( queryCollapseViewStates )
71+ . set ( { processingFinishedAtMs : 2 } )
72+ . where ( eq ( queryCollapseViewStates . sessionId , 's1' ) )
73+ . run ( ) ;
74+ expect ( await state . findProcessingByCurrentTurn ( 's1' , 't1' ) ) . toBeUndefined ( ) ;
75+ expect ( ( await state . findProcessingByCurrentTurn ( 's2' , 't1' ) ) ?. queryKey ) . toBe ( 'other' ) ;
76+ client . close ( ) ;
77+ const reopened = createQueryCollapseState ( { db : client . db } ) ;
78+ expect ( ( await reopened . findProcessingByCurrentTurn ( 's2' , 't1' ) ) ?. queryKey ) . toBe ( 'other' ) ;
79+ } ) ;
80+ } ) ;
81+
82+ it ( 'rereads receipts and validates corruption after a previous successful lookup' , async ( ) => {
83+ await withDatabase ( async ( client , writer ) => {
84+ const repository = createTurnRepository ( {
85+ db : client . db ,
86+ priorityFence : createQueueTurnAdmissionPriorityFence ( ) ,
87+ sessionAdmission : { rejectionInTransaction : ( ) => undefined } ,
88+ } ) ;
89+ expect ( await repository . findReceipt ( 'turn-1' ) ) . toBeUndefined ( ) ;
90+ for ( let i = 1 ; i <= 2 ; i += 1 ) {
91+ writer . db
92+ . insert ( turnIngress )
93+ . values ( {
94+ turnId : `turn-${ i } ` ,
95+ sessionId : `s${ i } ` ,
96+ busyReason : 'turn' ,
97+ inputJson : '{}' ,
98+ status : 'accepted' ,
99+ acceptedAtMs : 1 ,
100+ acceptedSequence : i ,
101+ inputDigest : `digest-${ i } ` ,
102+ } )
103+ . run ( ) ;
104+ }
105+ expect ( await repository . findReceipt ( 'turn-1' ) ) . toMatchObject ( {
106+ sessionId : 's1' ,
107+ acceptedSequence : 1 ,
108+ } ) ;
109+ expect ( await repository . findReceipt ( 'turn-2' ) ) . toMatchObject ( {
110+ sessionId : 's2' ,
111+ acceptedSequence : 2 ,
112+ } ) ;
113+ writer . db
114+ . update ( turnIngress )
115+ . set ( { inputDigest : '' } )
116+ . where ( eq ( turnIngress . turnId , 'turn-1' ) )
117+ . run ( ) ;
118+ await expect ( repository . findReceipt ( 'turn-1' ) ) . rejects . toThrow ( 'malformed' ) ;
119+ writer . db
120+ . update ( turnIngress )
121+ . set ( { inputDigest : 'edited' , acceptedSequence : 3 } )
122+ . where ( eq ( turnIngress . turnId , 'turn-1' ) )
123+ . run ( ) ;
124+ expect ( await repository . findReceipt ( 'turn-1' ) ) . toMatchObject ( {
125+ inputDigest : 'edited' ,
126+ acceptedSequence : 3 ,
127+ } ) ;
128+ writer . db . delete ( turnIngress ) . where ( eq ( turnIngress . turnId , 'turn-1' ) ) . run ( ) ;
129+ expect ( await repository . findReceipt ( 'turn-1' ) ) . toBeUndefined ( ) ;
130+ expect ( await repository . findReceipt ( "turn-2' OR 1=1 --" ) ) . toBeUndefined ( ) ;
131+ } ) ;
132+ } ) ;
133+ } ) ;
134+
21135describe ( 'native incremental semantic hashing' , ( ) => {
22136 const inputs = [ '' , 'abc' , '中文🙂' , '\ud800' , '\udc00' , 'a' . repeat ( 8191 ) + '🙂tail' ] ;
23137 for ( const length of [ 55 , 56 , 63 , 64 , 65 , 8191 , 8192 , 8193 , 32769 ] ) {
@@ -38,6 +152,63 @@ describe('native incremental semantic hashing', () => {
38152 createHash ( 'sha256' ) . update ( '\ud83d' ) . update ( '\ude42' ) . digest ( 'hex' ) ,
39153 ) ;
40154 } ) ;
155+ it ( 'matches native updates across repeated batches and split surrogate pairs' , ( ) => {
156+ const actual = new IncrementalSha256 ( ) ;
157+ const expected = createHash ( 'sha256' ) ;
158+ const parts = [ 'key' , ':' , '' , '\ud83d' , '\ude42' , '中文🙂' , 'x' . repeat ( 8191 ) , '🙂tail' ] ;
159+ for ( let index = 0 ; index < 257 ; index += 1 ) {
160+ for ( const part of parts ) {
161+ actual . update ( part ) ;
162+ expected . update ( part , 'utf8' ) ;
163+ }
164+ }
165+ expect ( actual . digestHex ( ) ) . toBe ( expected . digest ( 'hex' ) ) ;
166+ } ) ;
167+ } ) ;
168+
169+ describe ( 'streamed canonical history revisions' , ( ) => {
170+ it . each ( [ 0 , 1 , 100 ] ) ( 'preserves the canonical JSON digest for %i records' , ( length ) => {
171+ const records = Array . from ( { length } , ( _ , index ) => ( {
172+ message_id : `msg-${ index } ` ,
173+ turn_id : `turn-${ index } ` ,
174+ message : {
175+ role : 'user' ,
176+ timestamp : index ,
177+ content : '中文🙂\ud800' . repeat ( 2048 ) ,
178+ metadata : { z : [ null , true , - 0 ] , '10' : 'ten' , '2' : 'two' , a : { b : '"\\\n' } } ,
179+ } ,
180+ } ) ) ;
181+ const expected = `sha256:${ createHash ( 'sha256' )
182+ . update ( canonicalJson ( records . map ( decodeCanonicalHistoryEnvelope ) ) , 'utf8' )
183+ . digest ( 'hex' ) } `;
184+ expect ( canonicalHistoryRevision ( records ) ) . toBe ( expected ) ;
185+ expect ( canonicalActiveHistoryRevision ( records ) ) . toBe ( expected ) ;
186+ if ( records . length > 0 ) {
187+ records [ 0 ] ! . message . content = 'edited' ;
188+ expect ( canonicalHistoryRevision ( records ) ) . not . toBe ( expected ) ;
189+ }
190+ } ) ;
191+ it ( 'keeps active and settled sequence validation distinct' , ( ) => {
192+ const pending = [
193+ {
194+ message_id : 'msg-assistant' ,
195+ turn_id : 'turn-1' ,
196+ message : {
197+ role : 'assistant' ,
198+ timestamp : 1 ,
199+ content : [
200+ { type : 'toolCall' , id : 'call-1' , name : 'bash' , arguments : { command : 'pwd' } } ,
201+ ] ,
202+ } ,
203+ } ,
204+ ] ;
205+ const expected = `sha256:${ createHash ( 'sha256' )
206+ . update ( canonicalJson ( pending . map ( decodeCanonicalHistoryEnvelope ) ) , 'utf8' )
207+ . digest ( 'hex' ) } `;
208+ expect ( canonicalActiveHistoryRevision ( pending ) ) . toBe ( expected ) ;
209+ expect ( ( ) => canonicalHistoryRevision ( pending ) ) . toThrow ( 'tool results' ) ;
210+ expect ( ( ) => canonicalActiveHistoryRevision ( [ pending [ 0 ] ! , pending [ 0 ] ! ] ) ) . toThrow ( ) ;
211+ } ) ;
41212} ) ;
42213
43214describe ( 'semantic snapshots' , ( ) => {
@@ -486,6 +657,49 @@ describe('committed history read reuse', () => {
486657 expect ( readActive ) . not . toHaveBeenCalled ( ) ;
487658 expect ( ( await store . read ( 'synthetic-session' ) ) . revision ) . toBe ( 'r2' ) ;
488659 } ) ;
660+ it ( 'preserves owned history through the store and the next commit snapshot' , async ( ) => {
661+ const original = {
662+ revision : ' r1 ' ,
663+ messages : [ { role : 'user' , timestamp : 1 , content : 'original' . repeat ( 4096 ) } ] ,
664+ identityVector : [ 'msg-1' ] ,
665+ } ;
666+ const store = new DurableCanonicalHistoryStore ( {
667+ read : async ( ) => original ,
668+ readActive : async ( ) => original ,
669+ append : async ( ) => original ,
670+ replace : async ( ) => original ,
671+ } ) ;
672+ const committed = await store . append ( change ) ;
673+ const expectedContent = original . messages [ 0 ] ! . content ;
674+ original . messages [ 0 ] ! . content = 'changed' ;
675+ original . identityVector [ 0 ] = 'changed' ;
676+ expect ( committed . revision ) . toBe ( 'r1' ) ;
677+ expect ( committed . messages ) . toEqual ( [ { role : 'user' , timestamp : 1 , content : expectedContent } ] ) ;
678+ expect ( committed . identityVector ) . toEqual ( [ 'msg-1' ] ) ;
679+ expect ( Object . isFrozen ( committed . messages [ 0 ] ) ) . toBe ( true ) ;
680+ expect ( Object . isFrozen ( committed . messages ) ) . toBe ( true ) ;
681+ expect ( Object . isFrozen ( committed . identityVector ) ) . toBe ( true ) ;
682+ const clone = vi . spyOn ( globalThis , 'structuredClone' ) ;
683+ try {
684+ expect ( captureSemanticSnapshot ( committed ) . value ) . toBe ( committed ) ;
685+ expect (
686+ captureSemanticSnapshot ( { committedMessages : committed . messages } ) . value . committedMessages ,
687+ ) . toBe ( committed . messages ) ;
688+ expect ( clone ) . not . toHaveBeenCalled ( ) ;
689+ } finally {
690+ clone . mockRestore ( ) ;
691+ }
692+ const sharedEmpty : never [ ] = [ ] ;
693+ const emptyStore = new DurableCanonicalHistoryStore ( {
694+ read : async ( ) => ( { revision : 'r2' , messages : sharedEmpty , identityVector : sharedEmpty } ) ,
695+ readActive : async ( ) => original ,
696+ append : async ( ) => original ,
697+ replace : async ( ) => original ,
698+ } ) ;
699+ const empty = await emptyStore . read ( 'synthetic-session' ) ;
700+ expect ( empty . messages ) . not . toBe ( empty . identityVector ) ;
701+ expect ( empty . messages ) . toEqual ( [ ] ) ;
702+ } ) ;
489703 it ( 'retains legacy rereads and rejects invalid commits or write failures' , async ( ) => {
490704 const readActive = vi . fn ( async ( ) => ( {
491705 revision : 'r1' ,
0 commit comments