11import { Effect } from 'effect' ;
22import { createHash } from 'crypto' ;
33import { readFileSync } from 'fs' ;
4- import { resolve , dirname } from 'path' ;
4+ import { resolve } from 'path' ;
55import { ShadowGit } from './shadow-git.js' ;
66import { ProjectLock } from './project-lock.js' ;
77import { normalizePath } from '../core/path.js' ;
8- import { Ledger } from './ledger.js' ;
9- import { registerCheckpointHooks } from './hook-recorder.js' ;
10- import { HookService } from '../hooks/registry.js' ;
118import { shortSid , commitMsg } from './commit-naming.js' ;
129import { readRestoreEntry , writeRestoreEntry } from './restore-store.js' ;
13- import { classifyDiff , parseDiffStats } from './classification.js' ;
1410import {
1511 getCompletedTurnsFor ,
1612 getTurnRestorePlan ,
1713 getRollbackToTurnPlan ,
18- type RestorePlan ,
1914} from './restore-planning.js' ;
2015import { emptyRollbackResult , executeRollback } from './rollback-engine.js' ;
2116
@@ -25,7 +20,6 @@ export interface CheckpointDiff {
2520 turnId : number ;
2621 files : Array < {
2722 path : string ;
28- source : 'agent' | 'unknown' ;
2923 status : string ;
3024 diff : string ;
3125 insertions : number ;
@@ -58,12 +52,7 @@ export interface RollbackPreviewDiff {
5852export interface CodeRestoreEntry {
5953 id : string ;
6054 sessionId : string ;
61- action :
62- | 'checkpoint-file'
63- | 'checkpoint-files'
64- | 'checkpoint-agent'
65- | 'checkpoint-all'
66- | 'rollback-to-turn' ;
55+ action : 'checkpoint-file' | 'checkpoint-files' | 'rollback-to-turn' ;
6756 throughTurnId : number ;
6857 affectedTurns : number [ ] ;
6958 selectedFiles : string [ ] ;
@@ -97,12 +86,8 @@ export function hashWorkspaceFile(projectPath: string, file: string): string | n
9786
9887export class CheckpointService extends Effect . Service < CheckpointService > ( ) ( 'Checkpoint' , {
9988 effect : Effect . gen ( function * ( ) {
100- const hooks = yield * HookService ;
101- registerCheckpointHooks ( hooks ) ;
102-
10389 const shadowGitByProject = new Map < string , ShadowGit > ( ) ;
10490 const lockByProject = new Map < string , ProjectLock > ( ) ;
105- const ledgerByProject = new Map < string , { ledger : Ledger ; gitDir : string } > ( ) ;
10691
10792 function ensure ( projectPath : string ) : ShadowGit {
10893 const normalized = normalizePath ( projectPath ) ;
@@ -125,15 +110,20 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
125110 return lock ;
126111 }
127112
128- function ledger ( sg : ShadowGit ) : Ledger {
129- const key = sg . gitDir ;
130- let entry = ledgerByProject . get ( key ) ;
131- if ( ! entry || entry . gitDir !== key ) {
132- const l = new Ledger ( dirname ( sg . gitDir ) ) ;
133- entry = { ledger : l , gitDir : key } ;
134- ledgerByProject . set ( key , entry ) ;
113+ function repairIncompleteTurn ( sg : ShadowGit , sessionId : string ) : void {
114+ const completed = getCompletedTurnsFor ( sg , sessionId ) ;
115+ const candidate = completed . length > 0 ? completed [ completed . length - 1 ] ! + 1 : 1 ;
116+ const baseline = sg . findCommitByMessage ( commitMsg ( sessionId , candidate , 'baseline' ) ) ;
117+ if ( ! baseline ) return ;
118+ const final = sg . findCommitByMessage ( commitMsg ( sessionId , candidate , 'final' ) ) ;
119+ if ( final ) return ;
120+ const lock = lockFor ( sg . projectPath ) ;
121+ lock . lock ( ) ;
122+ try {
123+ sg . commit ( commitMsg ( sessionId , candidate , 'final' ) ) ;
124+ } finally {
125+ lock . unlock ( ) ;
135126 }
136- return entry . ledger ;
137127 }
138128
139129 return {
@@ -146,6 +136,7 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
146136 title ?: string
147137 ) : void => {
148138 const sg = ensure ( projectPath ) ;
139+ repairIncompleteTurn ( sg , sessionId ) ;
149140 if ( sg . isTooLargeForSnapshot ( ) ) return ;
150141 const lock = lockFor ( projectPath ) ;
151142 const msg = title
@@ -171,22 +162,11 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
171162 }
172163 } ,
173164
174- // ---- Classification ----
175-
176- classifyChanges : (
177- projectPath : string ,
178- sessionId : string ,
179- turnId : number
180- ) : { agentModified : string [ ] ; unknownSource : string [ ] } | null => {
181- const sg = ensure ( projectPath ) ;
182- const l = ledger ( sg ) ;
183- return classifyDiff ( projectPath , sessionId , turnId , sg , l ) ;
184- } ,
185-
186165 // ---- Query ----
187166
188167 getCompletedTurns : ( projectPath : string , sessionId : string ) : number [ ] => {
189168 const sg = ensure ( projectPath ) ;
169+ repairIncompleteTurn ( sg , sessionId ) ;
190170 return getCompletedTurnsFor ( sg , sessionId ) ;
191171 } ,
192172
@@ -196,18 +176,16 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
196176 ) : Array < {
197177 turnId : number ;
198178 title : string ;
199- agentModified : string [ ] ;
200- unknownSource : string [ ] ;
179+ files : string [ ] ;
201180 } > => {
202181 const sg = ensure ( projectPath ) ;
203- const l = ledger ( sg ) ;
182+ repairIncompleteTurn ( sg , sessionId ) ;
204183 const prefix = `turn-${ shortSid ( sessionId ) } -` ;
205184 const completedTurns = getCompletedTurnsFor ( sg , sessionId ) ;
206185 const result : Array < {
207186 turnId : number ;
208187 title : string ;
209- agentModified : string [ ] ;
210- unknownSource : string [ ] ;
188+ files : string [ ] ;
211189 } > = [ ] ;
212190
213191 for ( const i of completedTurns ) {
@@ -221,18 +199,9 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
221199 const title = fullMsg . includes ( ' ' ) ? fullMsg . split ( ' ' ) . slice ( 1 ) . join ( ' ' ) : '' ;
222200
223201 const allChanges = sg . diffFiles ( bCommit , fCommit ) ;
224- const rawAllFiles = allChanges . map ( ( c ) => normalizePath ( resolve ( projectPath , c . file ) ) ) ;
225- const allFiles = [ ...new Set ( rawAllFiles ) ] ;
226- const agentFiles = new Set (
227- l . getAgentFiles ( i , sessionId ) . map ( ( p ) => normalizePath ( p ) . toLowerCase ( ) )
228- ) ;
202+ const files = [ ...new Set ( allChanges . map ( ( c ) => normalizePath ( resolve ( projectPath , c . file ) ) ) ) ] ;
229203
230- result . push ( {
231- turnId : i ,
232- title,
233- agentModified : allFiles . filter ( ( f ) => agentFiles . has ( f . toLowerCase ( ) ) ) ,
234- unknownSource : allFiles . filter ( ( f ) => ! agentFiles . has ( f . toLowerCase ( ) ) ) ,
235- } ) ;
204+ result . push ( { turnId : i , title, files } ) ;
236205 }
237206 return result ;
238207 } ,
@@ -243,6 +212,7 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
243212 turnId ?: number
244213 ) : CheckpointDiff => {
245214 const sg = ensure ( projectPath ) ;
215+ repairIncompleteTurn ( sg , sessionId ) ;
246216 const completedTurns = getCompletedTurnsFor ( sg , sessionId ) ;
247217 const latestTurnId =
248218 turnId ?? ( completedTurns . length > 0 ? completedTurns [ completedTurns . length - 1 ] ! : 0 ) ;
@@ -257,29 +227,28 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
257227 const allChanges = sg . diffFiles ( baseline , final ) ;
258228 const rawAllFiles = allChanges . map ( ( c ) => normalizePath ( resolve ( projectPath , c . file ) ) ) ;
259229 const allFiles = [ ...new Set ( rawAllFiles ) ] ;
260- const agentFiles = new Set (
261- ledger ( sg )
262- . getAgentFiles ( latestTurnId , sessionId )
263- . map ( ( p ) => normalizePath ( p ) . toLowerCase ( ) )
264- ) ;
265230
266231 const files = allFiles . map ( ( f ) => {
267232 const relPath = toGitPath ( projectPath , f ) ;
268233 const diffResult = sg . git ( 'diff' , baseline , final , '--' , relPath ) ;
269234 const rawPath = normalizePath ( resolve ( projectPath , relPath ) ) ;
270- const stats = parseDiffStats ( diffResult . stdout ) ;
235+ let insertions = 0 ;
236+ let deletions = 0 ;
237+ for ( const line of diffResult . stdout . split ( '\n' ) ) {
238+ if ( line . startsWith ( '+' ) && ! line . startsWith ( '+++' ) ) insertions ++ ;
239+ else if ( line . startsWith ( '-' ) && ! line . startsWith ( '---' ) ) deletions ++ ;
240+ }
271241 return {
272242 path : f ,
273- source : ( agentFiles . has ( f . toLowerCase ( ) ) ? 'agent' : 'unknown' ) as 'agent' | 'unknown' ,
274243 status :
275244 allChanges . find (
276245 ( c ) =>
277246 normalizePath ( resolve ( projectPath , c . file ) ) . toLowerCase ( ) ===
278247 rawPath . toLowerCase ( )
279248 ) ?. status ?? 'M' ,
280249 diff : diffResult . stdout ,
281- insertions : stats . insertions ,
282- deletions : stats . deletions ,
250+ insertions,
251+ deletions,
283252 } ;
284253 } ) ;
285254
@@ -297,7 +266,7 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
297266 const sg = ensure ( projectPath ) ;
298267 const plan = getTurnRestorePlan ( sg , sessionId , turnId ) ;
299268 if ( ! plan ) {
300- return emptyRollbackResult ( turnId , turnId ) ;
269+ return emptyRollbackResult ( turnId ) ;
301270 }
302271 return executeRollback ( sessionId , plan , [ file ] , 'checkpoint-file' , sg , lockFor ( projectPath ) ) ;
303272 } ,
@@ -310,53 +279,10 @@ export class CheckpointService extends Effect.Service<CheckpointService>()('Chec
310279 ) : CodeRollbackResult => {
311280 const sg = ensure ( projectPath ) ;
312281 const plan = getTurnRestorePlan ( sg , sessionId , turnId ) ;
313- if ( ! plan ) {
314- return emptyRollbackResult ( turnId , turnId ) ;
315- }
316- return executeRollback ( sessionId , plan , files , 'checkpoint-files' , sg , lockFor ( projectPath ) ) ;
317- } ,
318-
319- revertCheckpointAgentFiles : (
320- projectPath : string ,
321- sessionId : string ,
322- turnId : number
323- ) : CodeRollbackResult => {
324- const sg = ensure ( projectPath ) ;
325- const l = ledger ( sg ) ;
326- const changes = classifyDiff ( projectPath , sessionId , turnId , sg , l ) ;
327- if ( ! changes ) {
328- return emptyRollbackResult ( turnId ) ;
329- }
330- if ( changes . agentModified . length === 0 ) {
331- return emptyRollbackResult ( turnId ) ;
332- }
333- const plan = getTurnRestorePlan ( sg , sessionId , turnId ) ;
334- if ( ! plan ) {
335- return emptyRollbackResult ( turnId ) ;
336- }
337- return executeRollback ( sessionId , plan , changes . agentModified , 'checkpoint-agent' , sg , lockFor ( projectPath ) ) ;
338- } ,
339-
340- revertCheckpointAllFiles : (
341- projectPath : string ,
342- sessionId : string ,
343- turnId : number
344- ) : CodeRollbackResult => {
345- const sg = ensure ( projectPath ) ;
346- const l = ledger ( sg ) ;
347- const changes = classifyDiff ( projectPath , sessionId , turnId , sg , l ) ;
348- if ( ! changes ) {
349- return emptyRollbackResult ( turnId ) ;
350- }
351- const all = [ ...changes . agentModified , ...changes . unknownSource ] ;
352- if ( all . length === 0 ) {
353- return emptyRollbackResult ( turnId ) ;
354- }
355- const plan = getTurnRestorePlan ( sg , sessionId , turnId ) ;
356282 if ( ! plan ) {
357283 return emptyRollbackResult ( turnId ) ;
358284 }
359- return executeRollback ( sessionId , plan , all , 'checkpoint-all ' , sg , lockFor ( projectPath ) ) ;
285+ return executeRollback ( sessionId , plan , files , 'checkpoint-files ' , sg , lockFor ( projectPath ) ) ;
360286 } ,
361287
362288 // ---- Rollback ----
0 commit comments