Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 126 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@ai-sdk/google": "^2.0.49",
"@ai-sdk/openai": "^2.0.88",
"@getzep/zep-cloud": "^3.13.0",
"@memvid/sdk": "^2.0.146",
"ai": "^5.0.115",
"drizzle-orm": "^0.45.1",
"mem0ai": "^2.1.38",
Expand Down
4 changes: 3 additions & 1 deletion src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import type { ConcurrencyConfig } from "../types/concurrency"
import { SupermemoryProvider } from "./supermemory"
import { Mem0Provider } from "./mem0"
import { ZepProvider } from "./zep"
import { MemvidProvider } from "./memvid"

const providers: Record<ProviderName, new () => Provider> = {
supermemory: SupermemoryProvider,
mem0: Mem0Provider,
zep: ZepProvider,
memvid: MemvidProvider,
}

export function createProvider(name: ProviderName): Provider {
Expand Down Expand Up @@ -35,4 +37,4 @@ export function getProviderInfo(name: ProviderName): {
}
}

export { SupermemoryProvider, Mem0Provider, ZepProvider }
export { SupermemoryProvider, Mem0Provider, ZepProvider, MemvidProvider }
134 changes: 134 additions & 0 deletions src/providers/memvid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type {
Provider,
ProviderConfig,
IngestOptions,
IngestResult,
SearchOptions,
IndexingProgressCallback,
} from "../../types/provider"
import type { UnifiedSession } from "../../types/unified"
import { logger } from "../../utils/logger"
import { open, create } from "@memvid/sdk"
import type { Memvid } from "@memvid/sdk"
import { MEMVID_PROMPTS } from "./prompts"
import path from "path"
import fs from "fs"

export class MemvidProvider implements Provider {
name = "memvid"
prompts = MEMVID_PROMPTS
private client: Memvid | null = null
private filePath: string = "memorybench.mv2"

async initialize(config: ProviderConfig): Promise<void> {
if (config.filePath) {
this.filePath = config.filePath as string
} else if (process.env.MEMVID_FILE_PATH) {
this.filePath = process.env.MEMVID_FILE_PATH
}

const dir = path.dirname(this.filePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}

try {
if (fs.existsSync(this.filePath)) {
this.client = await open(this.filePath)
logger.info(`Initialized Memvid provider (opened) at ${this.filePath}`)
} else {
this.client = await create(this.filePath)
logger.info(`Initialized Memvid provider (created) at ${this.filePath}`)
}
} catch (e) {
logger.error(`Failed to initialize Memvid: ${e}`)
throw e
}
}

async ingest(sessions: UnifiedSession[], options: IngestOptions): Promise<IngestResult> {
if (!this.client) throw new Error("Provider not initialized")

const documentIds: string[] = []

for (const session of sessions) {
const content = session.messages
.map((m) => `${m.role.toUpperCase()}: ${m.content}`)
.join("\n\n")

const uri = `mv2://session/${session.sessionId}`

try {
const frameId = await this.client.put({
title: `Session ${session.sessionId}`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The put method from the @memvid/sdk returns Promise<void>, but the code incorrectly expects it to return a document ID, resulting in frameId being undefined.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The put method from the @memvid/sdk is documented to return a Promise<void>, meaning it does not resolve with any value. However, the code at src/providers/memvid/index.ts:63 attempts to assign the result of this promise to a frameId variable. This variable will consequently be undefined. The code then proceeds to call String(frameId), which evaluates to the literal string "undefined". As a result, every document ingested through this provider will be assigned the same invalid document ID of "undefined", breaking any downstream functionality that relies on unique document tracking.

💡 Suggested Fix

Since the put method from the @memvid/sdk does not return a document ID, the logic for tracking ingested documents needs to be revised. Remove the assignment of the await this.client.put(...) result to frameId and adjust the documentIds array accordingly, as it's not possible to retrieve IDs from this specific call.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/providers/memvid/index.ts#L63

Potential issue: The `put` method from the `@memvid/sdk` is documented to return a
`Promise<void>`, meaning it does not resolve with any value. However, the code at
`src/providers/memvid/index.ts:63` attempts to assign the result of this promise to a
`frameId` variable. This variable will consequently be `undefined`. The code then
proceeds to call `String(frameId)`, which evaluates to the literal string "undefined".
As a result, every document ingested through this provider will be assigned the same
invalid document ID of "undefined", breaking any downstream functionality that relies on
unique document tracking.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8252104

text: content,
tags: [options.containerTag, ...(options.metadata?.tags as string[] || [])],
metadata: {
...options.metadata,
containerTag: options.containerTag,
sessionId: session.sessionId,
uri: uri
}
})

documentIds.push(String(frameId))
} catch (e) {
logger.error(`Failed to ingest session ${session.sessionId}: ${e}`)
}
}

return { documentIds }
}

async awaitIndexing(
result: IngestResult,
_containerTag: string,
onProgress?: IndexingProgressCallback
): Promise<void> {
onProgress?.({
completedIds: result.documentIds,
failedIds: [],
total: result.documentIds.length
})
}

async search(query: string, options: SearchOptions): Promise<unknown[]> {
if (!this.client) throw new Error("Provider not initialized")

try {
const result = await this.client.find(query, {
k: options.limit || 10,
// mode: "auto" // default
// scope?
})
return result.hits || []
} catch (e) {
logger.error(`Search failed: ${e}`)
return []
}
}

async clear(containerTag: string): Promise<void> {
if (!this.client) return

try {
if (this.client.seal) {
await this.client.seal()
}
this.client = null

if (fs.existsSync(this.filePath)) {
fs.unlinkSync(this.filePath)
logger.info(`Deleted Memvid file at ${this.filePath}`)
}

this.client = await create(this.filePath)
logger.info(`Re-initialized Memvid provider (created) at ${this.filePath}`)
} catch (e) {
logger.error(`Failed to clear Memvid: ${e}`)
if (!this.client && fs.existsSync(this.filePath)) {
this.client = await open(this.filePath)
}
}
}
}
38 changes: 38 additions & 0 deletions src/providers/memvid/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ProviderPrompts } from "../../types/prompts"

interface MemvidResult {
content?: string
text?: string
snippet?: string
title?: string
score?: number
created_at?: string
[key: string]: unknown
}

function buildMemvidContext(context: unknown[]): string {
return context.map((item) => {
const r = item as MemvidResult
const title = r.title || "Untitled"
const content = r.snippet || r.text || r.content || JSON.stringify(r)
return `Title: ${title}\nSnippet: ${content}`
}).join("\n\n")
}

export function buildMemvidAnswerPrompt(question: string, context: unknown[], questionDate?: string): string {
const contextStr = buildMemvidContext(context)

return `Based on the following context from the knowledge base, answer the question.

Context:
${contextStr}

Question: ${question}
Question Date: ${questionDate || "Not provided"}

Answer:`
}

export const MEMVID_PROMPTS: ProviderPrompts = {
answerPrompt: buildMemvidAnswerPrompt
}
2 changes: 1 addition & 1 deletion src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ export interface Provider {
clear(containerTag: string): Promise<void>
}

export type ProviderName = "supermemory" | "mem0" | "zep"
export type ProviderName = "supermemory" | "mem0" | "zep" | "memvid"
2 changes: 2 additions & 0 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export function getProviderConfig(provider: string): { apiKey: string; baseUrl?:
return { apiKey: config.mem0ApiKey }
case "zep":
return { apiKey: config.zepApiKey }
case "memvid":
return { apiKey: "" } // Memvid doesn't need api key for basic use
default:
throw new Error(`Unknown provider: ${provider}`)
}
Expand Down
9 changes: 9 additions & 0 deletions src/utils/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ export const MODEL_CONFIGS: Record<string, ModelConfig> = {
maxTokensParam: "maxTokens",
defaultMaxTokens: 1000,
},
"gemini-3-flash": {
id: "gemini-3-flash-preview",
provider: "google",
displayName: "Gemini 3 Flash",
supportsTemperature: true,
defaultTemperature: 1,
maxTokensParam: "maxTokens",
defaultMaxTokens: 1000,
},
}

export const DEFAULT_ANSWERING_MODEL = "gpt-4o"
Expand Down