import type { AdapterContext, MemoryCommitRequest, MemoryExportRequest, MemoryReadRequest, MemoryRevision, MemorySearchRequest, MemorySearchResult, MemorySnapshot, MemoryStore, PortableFile, } from "@rakazo/adapter-kit"; import { Prisma, type PrismaClient, withTransactionRetry } from "@rakazo/db"; export class MarkdownMemoryStore implements MemoryStore { constructor(private readonly prisma: PrismaClient) {} describe() { return { id: "markdown", contractVersion: "1", adapterVersion: "0.1.0", capabilities: { search: true, revisions: true, markdownPortable: true }, }; } async read(request: MemoryReadRequest, context: AdapterContext): Promise { const documents = await this.prisma.memoryDocument.findMany({ where: { spaceId: context.spaceId, userId: context.userId, scope: request.scope, ...(request.botId ? { botId: request.botId } : {}), ...(request.path ? { path: request.path } : {}), }, orderBy: [{ updatedAt: "desc" }, { path: "asc" }], }); return { documents: documents.map((doc) => ({ id: doc.id, path: doc.path, content: doc.content, revision: doc.revision, updatedAt: doc.updatedAt.toISOString(), })), }; } async search( request: MemorySearchRequest, context: AdapterContext, ): Promise { const documents = await this.prisma.memoryDocument.findMany({ where: { spaceId: context.spaceId, userId: context.userId, ...(request.scope === "all" ? {} : { scope: request.scope }), ...(request.botId ? { botId: request.botId } : {}), }, }); const q = request.query.toLowerCase(); return documents .filter((doc) => doc.content.toLowerCase().includes(q) || doc.path.toLowerCase().includes(q)) .map((doc) => ({ path: doc.path, snippet: snippet(doc.content, q), score: 1, })); } async commit(request: MemoryCommitRequest, context: AdapterContext): Promise { // A save replaces the current content and appends its history atomically. // Conflicts retry the whole transaction so revision numbers use fresh state. return withTransactionRetry(() => this.prisma.$transaction( async (tx) => { const existing = await tx.memoryDocument.findFirst({ where: { spaceId: context.spaceId, userId: context.userId, scope: request.scope, botId: request.botId ?? null, path: request.path, }, }); const doc = existing ? await tx.memoryDocument.update({ where: { id: existing.id }, data: { content: request.content, revision: existing.revision + 1 }, }) : await tx.memoryDocument.create({ data: { spaceId: context.spaceId, userId: context.userId, botId: request.botId, scope: request.scope, path: request.path, content: request.content, }, }); await tx.memoryRevision.create({ data: { documentId: doc.id, revision: doc.revision, content: request.content, sourceRunId: request.sourceRunId, sourceThreadId: request.sourceThreadId, }, }); return { id: doc.id, path: doc.path, revision: doc.revision, content: doc.content }; }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }, ), ); } async *exportMarkdown( request: MemoryExportRequest, context: AdapterContext, ): AsyncIterable { const snapshot = await this.read( { scope: request.scope === "all" ? "user" : request.scope, botId: request.botId }, context, ); for (const doc of snapshot.documents) { yield { path: doc.path, content: new TextEncoder().encode(doc.content) }; } } async importMarkdown( files: AsyncIterable, context: AdapterContext, ): Promise { let last: MemoryRevision | undefined; for await (const file of files) { last = await this.commit( { scope: "user", path: file.path, content: new TextDecoder().decode(file.content), }, context, ); } if (!last) throw new Error("No memory files to import"); return last; } } function snippet(content: string, q: string): string { const idx = content.toLowerCase().indexOf(q); if (idx < 0) return content.slice(0, 140); return content.slice(Math.max(0, idx - 40), idx + q.length + 80); }