@@ -47,7 +47,7 @@ import {
4747import { GraphTraverser , GraphQueryManager } from './graph' ;
4848import { VectorManager , createVectorManager , EmbeddingProgress } from './vectors' ;
4949import { ContextBuilder , createContextBuilder } from './context' ;
50- import { Mutex } from './utils' ;
50+ import { Mutex , FileLock } from './utils' ;
5151
5252// Re-export types for consumers
5353export * from './types' ;
@@ -77,7 +77,7 @@ export {
7777 silentLogger ,
7878 defaultLogger ,
7979} from './errors' ;
80- export { Mutex , processInBatches , debounce , throttle , MemoryMonitor } from './utils' ;
80+ export { Mutex , FileLock , processInBatches , debounce , throttle , MemoryMonitor } from './utils' ;
8181export { MCPServer } from './mcp' ;
8282
8383/**
@@ -133,9 +133,12 @@ export class CodeGraph {
133133 private vectorManager : VectorManager | null = null ;
134134 private contextBuilder : ContextBuilder ;
135135
136- // Mutex for preventing concurrent indexing operations
136+ // Mutex for preventing concurrent indexing operations (in-process)
137137 private indexMutex = new Mutex ( ) ;
138138
139+ // File lock for preventing concurrent writes across processes (CLI, MCP, git hooks)
140+ private fileLock : FileLock ;
141+
139142 private constructor (
140143 db : DatabaseConnection ,
141144 queries : QueryBuilder ,
@@ -146,6 +149,7 @@ export class CodeGraph {
146149 this . queries = queries ;
147150 this . config = config ;
148151 this . projectRoot = projectRoot ;
152+ this . fileLock = new FileLock ( db . getPath ( ) ) ;
149153 this . orchestrator = new ExtractionOrchestrator ( projectRoot , config , queries ) ;
150154 this . resolver = createResolver ( projectRoot , queries ) ;
151155 this . graphManager = new GraphQueryManager ( queries ) ;
@@ -317,6 +321,8 @@ export class CodeGraph {
317321 * Close the CodeGraph instance and release resources
318322 */
319323 close ( ) : void {
324+ // Release file lock if held
325+ this . fileLock . release ( ) ;
320326 // Dispose vector manager first to release ONNX workers
321327 if ( this . vectorManager ) {
322328 this . vectorManager . dispose ( ) ;
@@ -369,29 +375,37 @@ export class CodeGraph {
369375 */
370376 async indexAll ( options : IndexOptions = { } ) : Promise < IndexResult > {
371377 return this . indexMutex . withLock ( async ( ) => {
372- const result = await this . orchestrator . indexAll ( options . onProgress , options . signal ) ;
373-
374- // Resolve references to create call/import/extends edges
375- if ( result . success && result . filesIndexed > 0 ) {
376- // Get count of unresolved references for accurate progress
377- const unresolvedCount = this . queries . getUnresolvedReferences ( ) . length ;
378+ const locked = await this . fileLock . acquire ( ) ;
379+ if ( ! locked ) {
380+ return { success : false , filesIndexed : 0 , filesSkipped : 0 , nodesCreated : 0 , edgesCreated : 0 , errors : [ { message : 'Could not acquire file lock - another process may be indexing' , severity : 'error' as const } ] , durationMs : 0 } ;
381+ }
382+ try {
383+ const result = await this . orchestrator . indexAll ( options . onProgress , options . signal ) ;
378384
379- options . onProgress ?.( {
380- phase : 'resolving' ,
381- current : 0 ,
382- total : unresolvedCount ,
383- } ) ;
385+ // Resolve references to create call/import/extends edges
386+ if ( result . success && result . filesIndexed > 0 ) {
387+ // Get count of unresolved references for accurate progress
388+ const unresolvedCount = this . queries . getUnresolvedReferences ( ) . length ;
384389
385- this . resolveReferences ( ( current , total ) => {
386390 options . onProgress ?.( {
387391 phase : 'resolving' ,
388- current,
389- total,
392+ current : 0 ,
393+ total : unresolvedCount ,
390394 } ) ;
391- } ) ;
392- }
393395
394- return result ;
396+ this . resolveReferences ( ( current , total ) => {
397+ options . onProgress ?.( {
398+ phase : 'resolving' ,
399+ current,
400+ total,
401+ } ) ;
402+ } ) ;
403+ }
404+
405+ return result ;
406+ } finally {
407+ this . fileLock . release ( ) ;
408+ }
395409 } ) ;
396410 }
397411
@@ -402,7 +416,15 @@ export class CodeGraph {
402416 */
403417 async indexFiles ( filePaths : string [ ] ) : Promise < IndexResult > {
404418 return this . indexMutex . withLock ( async ( ) => {
405- return this . orchestrator . indexFiles ( filePaths ) ;
419+ const locked = await this . fileLock . acquire ( ) ;
420+ if ( ! locked ) {
421+ return { success : false , filesIndexed : 0 , filesSkipped : 0 , nodesCreated : 0 , edgesCreated : 0 , errors : [ { message : 'Could not acquire file lock - another process may be indexing' , severity : 'error' as const } ] , durationMs : 0 } ;
422+ }
423+ try {
424+ return this . orchestrator . indexFiles ( filePaths ) ;
425+ } finally {
426+ this . fileLock . release ( ) ;
427+ }
406428 } ) ;
407429 }
408430
@@ -413,28 +435,36 @@ export class CodeGraph {
413435 */
414436 async sync ( options : IndexOptions = { } ) : Promise < SyncResult > {
415437 return this . indexMutex . withLock ( async ( ) => {
416- const result = await this . orchestrator . sync ( options . onProgress ) ;
417-
418- // Resolve references if files were updated
419- if ( result . filesAdded > 0 || result . filesModified > 0 ) {
420- const unresolvedCount = this . queries . getUnresolvedReferences ( ) . length ;
438+ const locked = await this . fileLock . acquire ( ) ;
439+ if ( ! locked ) {
440+ return { filesChecked : 0 , filesAdded : 0 , filesModified : 0 , filesRemoved : 0 , nodesUpdated : 0 , durationMs : 0 } ;
441+ }
442+ try {
443+ const result = await this . orchestrator . sync ( options . onProgress ) ;
421444
422- options . onProgress ?.( {
423- phase : 'resolving' ,
424- current : 0 ,
425- total : unresolvedCount ,
426- } ) ;
445+ // Resolve references if files were updated
446+ if ( result . filesAdded > 0 || result . filesModified > 0 ) {
447+ const unresolvedCount = this . queries . getUnresolvedReferences ( ) . length ;
427448
428- this . resolveReferences ( ( current , total ) => {
429449 options . onProgress ?.( {
430450 phase : 'resolving' ,
431- current,
432- total,
451+ current : 0 ,
452+ total : unresolvedCount ,
433453 } ) ;
434- } ) ;
435- }
436454
437- return result ;
455+ this . resolveReferences ( ( current , total ) => {
456+ options . onProgress ?.( {
457+ phase : 'resolving' ,
458+ current,
459+ total,
460+ } ) ;
461+ } ) ;
462+ }
463+
464+ return result ;
465+ } finally {
466+ this . fileLock . release ( ) ;
467+ }
438468 } ) ;
439469 }
440470
0 commit comments