Skip to content

Commit d681ee5

Browse files
add dimension mismatch detection and version injection to memory plugin
1 parent be871aa commit d681ee5

7 files changed

Lines changed: 111 additions & 17 deletions

File tree

packages/memory/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opencode-manager/memory",
3-
"version": "0.0.4",
3+
"version": "0.0.5",
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -49,7 +49,7 @@
4949
"typescript": "^5.7.3"
5050
},
5151
"scripts": {
52-
"build": "tsc -p tsconfig.build.json",
52+
"build": "bun scripts/inject-version.ts && tsc -p tsconfig.build.json",
5353
"postinstall": "node scripts/download-models.js",
5454
"prepublishOnly": "pnpm build",
5555
"test": "bun test",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { readFileSync, writeFileSync } from 'fs'
2+
import { join } from 'path'
3+
4+
const packageJsonPath = join(__dirname, '..', 'package.json')
5+
const versionPath = join(__dirname, '..', 'src', 'version.ts')
6+
7+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
8+
const version = packageJson.version as string
9+
10+
const versionContent = `export const VERSION = '${version}'\n`
11+
12+
writeFileSync(versionPath, versionContent, 'utf-8')
13+
14+
console.log(`Version ${version} written to src/version.ts`)

packages/memory/src/embedding/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { LocalEmbeddingProvider } from './local'
66
import { SharedEmbeddingClient } from './client'
77
import { resolveDataDir } from '../storage/database'
88
import type { CacheService } from '../cache/types'
9-
export { checkServerHealth, isServerRunning } from './shared'
9+
export { checkServerHealth, isServerRunning, killEmbeddingServer } from './shared'
1010

1111
export type { EmbeddingProvider } from './types'
1212

packages/memory/src/index.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,27 @@ import type { Plugin, PluginInput, Hooks } from '@opencode-ai/plugin'
22
import { tool } from '@opencode-ai/plugin'
33
import { agents } from './agents'
44
import { createConfigHandler } from './config'
5+
import { VERSION } from './version'
56
import {
67
createSessionHooks,
78
createKeywordHooks,
89
createParamsHooks,
910
ACTIVATION_CONTEXT,
1011
} from './hooks'
1112
import { join } from 'path'
12-
import { initializeDatabase, resolveDataDir, closeDatabase, createMetadataQuery } from './storage'
13+
import { initializeDatabase, resolveDataDir, closeDatabase, createMetadataQuery, getTableDimensions, recreateVecTable } from './storage'
1314
import type { MemoryService } from './services/memory'
1415
import { createVecService } from './storage/vec'
15-
import { createEmbeddingProvider, checkServerHealth, isServerRunning } from './embedding'
16+
import { createEmbeddingProvider, checkServerHealth, isServerRunning, killEmbeddingServer } from './embedding'
1617
import { createMemoryService } from './services/memory'
1718
import { createSessionStateService } from './services/session-state'
1819
import { createEmbeddingSyncService } from './services/embedding-sync'
1920
import { loadPluginConfig } from './setup'
2021
import { resolveLogPath } from './storage'
2122
import { createLogger } from './utils/logger'
23+
import type { Database } from 'bun:sqlite'
2224
import type { PluginConfig, CompactionConfig, HealthStatus, Logger, PlanningState } from './types'
2325
import type { EmbeddingProvider } from './embedding'
24-
import type { Database } from 'bun:sqlite'
2526
import { createNoopVecService } from './storage/vec'
2627

2728

@@ -108,7 +109,8 @@ function formatHealthStatus(status: HealthStatus, provider: EmbeddingProvider):
108109
const embeddingStatus: 'ok' | 'error' = operational ? 'ok' : 'error'
109110

110111
const lines: string[] = [
111-
`Memory Plugin Health: ${overallStatus.toUpperCase()}`,
112+
`Memory Plugin v${VERSION}`,
113+
`Status: ${overallStatus.toUpperCase()}`,
112114
'',
113115
`Embedding: ${embeddingStatus}`,
114116
` Provider: ${provider.name} (${provider.dimensions}d)`,
@@ -150,11 +152,19 @@ async function executeHealthCheck(
150152
return formatHealthStatus(status, provider)
151153
}
152154

155+
interface DimensionMismatchState {
156+
detected: boolean
157+
expected: number | null
158+
actual: number | null
159+
}
160+
153161
async function executeReindex(
154162
memoryService: MemoryService,
155163
db: Database,
156164
config: PluginConfig,
157165
provider: EmbeddingProvider,
166+
dataDir: string,
167+
mismatchState: DimensionMismatchState,
158168
): Promise<string> {
159169
const configuredModel = config.embedding.model
160170
const configuredDimensions = config.embedding.dimensions ?? provider.dimensions
@@ -170,13 +180,26 @@ async function executeReindex(
170180
return 'Reindex failed: embedding provider is not operational. Check your API key and model configuration.'
171181
}
172182

183+
const tableResult = getTableDimensions(db)
184+
if (tableResult.exists && tableResult.dimensions !== configuredDimensions) {
185+
recreateVecTable(db, configuredDimensions)
186+
const newVec = await createVecService(db, dataDir, configuredDimensions)
187+
memoryService.setVecService(newVec)
188+
}
189+
173190
const result = await memoryService.reindex()
174191

175192
if (result.success > 0 || result.total === 0) {
176193
const metadata = createMetadataQuery(db)
177194
metadata.setEmbeddingModel(configuredModel, configuredDimensions)
178195
}
179196

197+
if (result.failed === 0) {
198+
mismatchState.detected = false
199+
mismatchState.expected = null
200+
mismatchState.actual = null
201+
}
202+
180203
const lines: string[] = [
181204
'Reindex complete',
182205
'',
@@ -248,6 +271,11 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
248271
provider.warmup()
249272

250273
const dataDir = config.dataDir ?? resolveDataDir()
274+
275+
if (config.embedding.provider !== 'local') {
276+
killEmbeddingServer(dataDir).catch(() => {})
277+
}
278+
251279
const db = initializeDatabase(dataDir)
252280
const dimensions = config.embedding.dimensions ?? provider.dimensions
253281

@@ -267,6 +295,12 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
267295
memoryService.setDedupThreshold(config.dedupThreshold)
268296
}
269297

298+
const mismatchState: DimensionMismatchState = {
299+
detected: false,
300+
expected: null,
301+
actual: null,
302+
}
303+
270304
const initPromise = createVecService(db, dataDir, dimensions)
271305
.then(async (vec) => {
272306
memoryService.setVecService(vec)
@@ -278,6 +312,16 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
278312

279313
logger.log('Vec service initialized')
280314

315+
const tableResult = getTableDimensions(db)
316+
if (tableResult.exists && tableResult.dimensions !== dimensions) {
317+
mismatchState.detected = true
318+
mismatchState.expected = dimensions
319+
mismatchState.actual = tableResult.dimensions
320+
memoryService.setVecService(createNoopVecService())
321+
logger.log(`Dimension mismatch detected: config=${dimensions}, table=${tableResult.dimensions ?? 'unknown'}`)
322+
return
323+
}
324+
281325
const embeddingSync = createEmbeddingSyncService(memoryService, logger)
282326
await embeddingSync.start().catch((err: unknown) => {
283327
logger.error('Embedding sync failed', err)
@@ -296,6 +340,11 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
296340

297341
const scopeEnum = z.enum(['convention', 'decision', 'context'])
298342

343+
function withDimensionWarning(result: string): string {
344+
if (!mismatchState.detected) return result
345+
return `${result}\n\n---\nWarning: Embedding dimension mismatch detected (config: ${mismatchState.expected}d, database: ${mismatchState.actual}d). Semantic search is disabled.\n- If you changed your embedding model intentionally, run memory-health with action "reindex" to rebuild embeddings.\n- If this was accidental, revert your embedding config to match the existing model.`
346+
}
347+
299348
let cleaned = false
300349
const cleanup = async () => {
301350
if (cleaned) return
@@ -343,13 +392,13 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
343392

344393
logger.log(`memory-read: returned ${results.length} results`)
345394
if (results.length === 0) {
346-
return 'No memories found.'
395+
return withDimensionWarning('No memories found.')
347396
}
348397

349398
const formatted = results.map(
350399
(m: any) => `[${m.id}] (${m.scope}) - Created ${new Date(m.createdAt).toISOString().split('T')[0]}\n${m.content}`
351400
)
352-
return `Found ${results.length} memories:\n\n${formatted.join('\n\n')}`
401+
return withDimensionWarning(`Found ${results.length} memories:\n\n${formatted.join('\n\n')}`)
353402
},
354403
}),
355404
'memory-write': tool({
@@ -369,7 +418,7 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
369418
})
370419

371420
logger.log(`memory-write: created id=${result.id}, deduplicated=${result.deduplicated}`)
372-
return `Memory stored (ID: #${result.id}, scope: ${args.scope}).${result.deduplicated ? ' (matched existing memory)' : ''}`
421+
return withDimensionWarning(`Memory stored (ID: #${result.id}, scope: ${args.scope}).${result.deduplicated ? ' (matched existing memory)' : ''}`)
373422
},
374423
}),
375424
'memory-edit': tool({
@@ -386,7 +435,7 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
386435
const memory = memoryService.getById(args.id)
387436
if (!memory) {
388437
logger.log(`memory-edit: id=${args.id} not found`)
389-
return `Memory #${args.id} not found.`
438+
return withDimensionWarning(`Memory #${args.id} not found.`)
390439
}
391440

392441
await memoryService.update(args.id, {
@@ -395,7 +444,7 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
395444
})
396445

397446
logger.log(`memory-edit: updated id=${args.id}`)
398-
return `Updated memory #${args.id} (scope: ${args.scope ?? memory.scope}).`
447+
return withDimensionWarning(`Updated memory #${args.id} (scope: ${args.scope ?? memory.scope}).`)
399448
},
400449
}),
401450
'memory-delete': tool({
@@ -404,31 +453,32 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
404453
id: z.number().describe('The memory ID to delete'),
405454
},
406455
execute: async (args) => {
456+
await initPromise
407457
const id = args.id
408458
logger.log(`memory-delete: id=${id}`)
409459

410460
const memory = memoryService.getById(id)
411461
if (!memory) {
412462
logger.log(`memory-delete: id=${id} not found`)
413-
return `Memory #${id} not found.`
463+
return withDimensionWarning(`Memory #${id} not found.`)
414464
}
415465

416466
await memoryService.delete(id)
417467
logger.log(`memory-delete: deleted id=${id}`)
418-
return `Deleted memory #${id}: "${memory.content.substring(0, 50)}..." (${memory.scope})`
468+
return withDimensionWarning(`Deleted memory #${id}: "${memory.content.substring(0, 50)}..." (${memory.scope})`)
419469
},
420470
}),
421471
'memory-health': tool({
422-
description: 'Check memory plugin health or trigger a reindex of all embeddings. Use action "check" (default) to view status, or "reindex" to regenerate all embeddings when model has changed or embeddings are missing.',
472+
description: 'Check memory plugin health or trigger a reindex of all embeddings. Use action "check" (default) to view status, or "reindex" to regenerate all embeddings when model has changed or embeddings are missing. Always report the plugin version from the output. Never run reindex unless the user explicitly asks for it.',
423473
args: {
424474
action: z.enum(['check', 'reindex']).optional().default('check').describe('Action to perform: "check" for health status, "reindex" to regenerate embeddings'),
425475
},
426476
execute: async (args) => {
427477
await initPromise
428478
if (args.action === 'reindex') {
429-
return executeReindex(memoryService, db, config, provider)
479+
return executeReindex(memoryService, db, config, provider, dataDir, mismatchState)
430480
}
431-
return executeHealthCheck(db, config, provider, dataDir)
481+
return withDimensionWarning(await executeHealthCheck(db, config, provider, dataDir))
432482
},
433483
}),
434484
'memory-planning-update': tool({

packages/memory/src/storage/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export { initializeDatabase, closeDatabase, resolveDataDir, resolveLogPath } from './database'
22
export { createVecService } from './vec'
3+
export { getTableDimensions, recreateVecTable } from './vec-utils'
4+
export type { TableDimensionsResult } from './vec-utils'
35
export type { VecService, VecSearchResult } from './vec-types'
46
export { createMemoryQuery } from './memory-queries'
57
export { createSessionStateQueries } from './session-state-queries'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Database } from 'bun:sqlite'
2+
3+
export type TableDimensionsResult =
4+
| { exists: false }
5+
| { exists: true; dimensions: number | null }
6+
7+
export function getTableDimensions(db: Database): TableDimensionsResult {
8+
const row = db.prepare(
9+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='memory_embeddings'"
10+
).get() as { sql: string } | undefined
11+
12+
if (!row) return { exists: false }
13+
14+
const match = row.sql.match(/float\[(\d+)\]/i)
15+
return { exists: true, dimensions: match ? parseInt(match[1]!, 10) : null }
16+
}
17+
18+
export function recreateVecTable(db: Database, dimensions: number): void {
19+
db.run('DROP TABLE IF EXISTS memory_embeddings')
20+
db.run(`
21+
CREATE VIRTUAL TABLE memory_embeddings USING vec0(
22+
embedding float[${dimensions}],
23+
+memory_id INTEGER,
24+
+project_id TEXT
25+
)
26+
`)
27+
}

packages/memory/src/version.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const VERSION = '0.0.5'

0 commit comments

Comments
 (0)