Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
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<MemorySnapshot> {
|
|
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<MemorySearchResult[]> {
|
|
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<MemoryRevision> {
|
|
// 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<PortableFile> {
|
|
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<PortableFile>,
|
|
context: AdapterContext,
|
|
): Promise<MemoryRevision> {
|
|
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);
|
|
}
|