Skip to content

Commit 86615d1

Browse files
Bump memory package version and enhance cleanup
1 parent 4acec3a commit 86615d1

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed

packages/memory/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opencode-manager/memory",
3-
"version": "0.0.18",
3+
"version": "0.0.19",
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -38,6 +38,7 @@
3838
"dependencies": {
3939
"@huggingface/transformers": "^3.8.1",
4040
"@opencode-ai/plugin": "^1.2.16",
41+
"@opencode-ai/sdk": "^1.2.26",
4142
"sqlite-vec": "0.1.7-alpha.2"
4243
},
4344
"optionalDependencies": {

packages/memory/src/cli/commands/cleanup.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { 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

38
interface CleanupOptions {
49
olderThan?: number
@@ -10,6 +15,7 @@ interface CleanupOptions {
1015
projectId?: string
1116
dbPath?: string
1217
help?: boolean
18+
vecWorkers?: boolean
1319
}
1420

1521
function 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
7179
Usage:
7280
ocm-mem cleanup [options]
81+
ocm-mem cleanup --vec-workers
7382
7483
Options:
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(/--db\s+([^\s]+)/)
267+
const socketMatch = line.match(/--socket\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+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, test, expect } from 'vitest'
2+
import { stripPromiseTags } from '../src/utils/strip-promise-tags'
3+
4+
describe('stripPromiseTags', () => {
5+
test('returns unchanged text when no promise tags present', () => {
6+
const text = 'This is a normal plan without any special tags'
7+
const { cleaned, stripped } = stripPromiseTags(text)
8+
expect(cleaned).toBe(text)
9+
expect(stripped).toBe(false)
10+
})
11+
12+
test('strips bare promise tags', () => {
13+
const text = 'Plan text here <promise>All phases of the plan have been completed successfully</promise>'
14+
const { cleaned, stripped } = stripPromiseTags(text)
15+
expect(cleaned).toBe('Plan text here')
16+
expect(stripped).toBe(true)
17+
expect(cleaned).not.toContain('<promise>')
18+
})
19+
20+
test('strips full instruction block with promise tags', () => {
21+
const text = `Plan text here
22+
23+
---
24+
25+
**IMPORTANT - Completion Signal:** When you have completed ALL phases of this plan successfully, you MUST output the following tag exactly: <promise>All phases of the plan have been completed successfully</promise>
26+
27+
Do NOT output this tag until every phase is truly complete. The loop will continue until this signal is detected.`
28+
const { cleaned, stripped } = stripPromiseTags(text)
29+
expect(cleaned).toBe('Plan text here')
30+
expect(stripped).toBe(true)
31+
expect(cleaned).not.toContain('<promise>')
32+
expect(cleaned).not.toContain('Completion Signal')
33+
})
34+
35+
test('preserves plan content before promise tags', () => {
36+
const plan = `## Phase 1
37+
Do something
38+
39+
## Phase 2
40+
Do something else
41+
42+
<promise>DONE</promise>`
43+
const { cleaned, stripped } = stripPromiseTags(plan)
44+
expect(cleaned).toContain('## Phase 1')
45+
expect(cleaned).toContain('## Phase 2')
46+
expect(cleaned).not.toContain('<promise>')
47+
expect(stripped).toBe(true)
48+
})
49+
50+
test('handles promise tags with multiline content', () => {
51+
const text = 'Plan <promise>\nMulti\nLine\nContent\n</promise> end'
52+
const { cleaned, stripped } = stripPromiseTags(text)
53+
expect(cleaned).not.toContain('<promise>')
54+
expect(stripped).toBe(true)
55+
})
56+
})

0 commit comments

Comments
 (0)