11import { openDatabase , formatDate , truncate , confirm , resolveProjectNames , displayProjectId , MemoryScope } from '../utils'
2+ import { existsSync , readFileSync } from 'fs'
3+ import { join } from 'path'
4+ import { homedir , platform } from 'os'
5+ import { execSync } from 'child_process'
6+ import { createConnection } from 'net'
27
38interface CleanupOptions {
49 olderThan ?: number
@@ -10,6 +15,7 @@ interface CleanupOptions {
1015 projectId ?: string
1116 dbPath ?: string
1217 help ?: boolean
18+ vecWorkers ?: boolean
1319}
1420
1521function parseArgs ( args : string [ ] ) : CleanupOptions {
@@ -49,6 +55,8 @@ function parseArgs(args: string[]): CleanupOptions {
4955 options . projectId = args [ ++ i ]
5056 } else if ( arg === '--db-path' ) {
5157 options . dbPath = args [ ++ i ]
58+ } else if ( arg === '--vec-workers' ) {
59+ options . vecWorkers = true
5260 } else if ( arg === '--help' || arg === '-h' ) {
5361 help ( )
5462 process . exit ( 0 )
@@ -70,6 +78,7 @@ Delete memories by criteria
7078
7179Usage:
7280 ocm-mem cleanup [options]
81+ ocm-mem cleanup --vec-workers
7382
7483Options:
7584 --older-than <days> Delete memories older than N days
@@ -80,11 +89,12 @@ Options:
8089 --force Skip confirmation prompt
8190 --project, -p <id> Project ID (auto-detected from git if not provided)
8291 --db-path <path> Path to memory database
92+ --vec-workers Clean up orphaned vec-worker processes
8393 --help, -h Show this help message
8494 ` . trim ( ) )
8595}
8696
87- export function run ( args : string [ ] , globalOpts : { dbPath ?: string ; projectId ?: string } ) : void {
97+ export async function run ( args : string [ ] , globalOpts : { dbPath ?: string ; projectId ?: string } ) : Promise < void > {
8898 const options = parseArgs ( args )
8999 options . projectId = options . projectId || globalOpts . projectId
90100
@@ -93,6 +103,12 @@ export function run(args: string[], globalOpts: { dbPath?: string; projectId?: s
93103 process . exit ( 0 )
94104 }
95105
106+ if ( options . vecWorkers ) {
107+ const result = await cleanupVecWorkers ( )
108+ console . log ( result )
109+ return
110+ }
111+
96112 if ( ! options . olderThan && ! options . ids && ! options . all ) {
97113 console . error ( 'At least one filter must be provided: --older-than, --ids, or --all' )
98114 help ( )
@@ -198,3 +214,104 @@ async function runMemoryCleanup(db: ReturnType<typeof openDatabase>, options: Cl
198214 console . log ( `Deleted ${ rows . length } memories. ${ remainingCount . count } remaining.` )
199215 console . log ( "Note: Run 'memory-health reindex' in OpenCode to clean up orphaned embeddings." )
200216}
217+
218+ export async function cleanupVecWorkers ( ) : Promise < string > {
219+ const workers = findVecWorkers ( )
220+ const defaultDataDir = getDefaultDataDir ( )
221+
222+ if ( workers . length === 0 ) {
223+ return 'No vec-worker processes found.'
224+ }
225+
226+ const results : string [ ] = [ ]
227+ let cleaned = 0
228+
229+ for ( const worker of workers ) {
230+ const isDefault = worker . dbPath . startsWith ( defaultDataDir )
231+ const isHealthy = await isWorkerHealthy ( worker . pid , worker . socketPath )
232+
233+ if ( isHealthy ) {
234+ results . push ( `✓ PID ${ worker . pid } - healthy (data dir: ${ isDefault ? 'global' : 'workspace' } )` )
235+ } else {
236+ try {
237+ process . kill ( worker . pid , 'SIGTERM' )
238+ results . push ( `✗ PID ${ worker . pid } - terminated (was orphaned)` )
239+ cleaned ++
240+ } catch ( err ) {
241+ results . push ( `✗ PID ${ worker . pid } - failed to terminate` )
242+ }
243+ }
244+ }
245+
246+ return `Vec-worker cleanup complete:\n${ results . join ( '\n' ) } \n\nTerminated ${ cleaned } orphaned worker(s).`
247+ }
248+
249+ function getDefaultDataDir ( ) : string {
250+ const defaultBase = join ( homedir ( ) , platform ( ) === 'win32' ? 'AppData' : '.local' , 'share' )
251+ const xdgDataHome = process . env [ 'XDG_DATA_HOME' ] || defaultBase
252+ return join ( xdgDataHome , 'opencode' , 'memory' )
253+ }
254+
255+ function findVecWorkers ( ) : Array < { pid : number ; dbPath : string ; socketPath : string } > {
256+ const workers : Array < { pid : number ; dbPath : string ; socketPath : string } > = [ ]
257+
258+ try {
259+ const output = execSync ( 'ps aux | grep vec-worker | grep -v grep' , { encoding : 'utf-8' } )
260+ const lines = output . split ( '\n' ) . filter ( line => line . trim ( ) )
261+
262+ for ( const line of lines ) {
263+ const parts = line . trim ( ) . split ( / \s + / )
264+ const pid = parseInt ( parts [ 1 ] , 10 )
265+
266+ const dbMatch = line . match ( / - - d b \s + ( [ ^ \s ] + ) / )
267+ const socketMatch = line . match ( / - - s o c k e t \s + ( [ ^ \s ] + ) / )
268+
269+ if ( dbMatch && socketMatch && ! isNaN ( pid ) ) {
270+ workers . push ( {
271+ pid,
272+ dbPath : dbMatch [ 1 ] ,
273+ socketPath : socketMatch [ 1 ] ,
274+ } )
275+ }
276+ }
277+ } catch {
278+ }
279+
280+ return workers
281+ }
282+
283+ async function isWorkerHealthy ( pid : number , socketPath : string ) : Promise < boolean > {
284+ if ( ! existsSync ( socketPath ) ) return false
285+ try {
286+ process . kill ( pid , 0 )
287+ return new Promise ( ( resolve ) => {
288+ const client = createConnection ( { path : socketPath } )
289+ const timeout = setTimeout ( ( ) => {
290+ client . destroy ( )
291+ resolve ( false )
292+ } , 2000 )
293+
294+ client . on ( 'connect' , ( ) => {
295+ client . write ( JSON . stringify ( { action : 'health' } ) + '\n' )
296+ } )
297+
298+ client . on ( 'data' , ( chunk ) => {
299+ clearTimeout ( timeout )
300+ client . destroy ( )
301+ try {
302+ const response = JSON . parse ( chunk . toString ( ) )
303+ resolve ( response . status === 'ok' )
304+ } catch {
305+ resolve ( false )
306+ }
307+ } )
308+
309+ client . on ( 'error' , ( ) => {
310+ clearTimeout ( timeout )
311+ resolve ( false )
312+ } )
313+ } )
314+ } catch {
315+ return false
316+ }
317+ }
0 commit comments