evOo-local/packages/db/src/messages.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
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.
2026-09-20 17:15:16 +02:00

109 lines
2.8 KiB
TypeScript

import type { MessageBlock } from "@rakazo/contracts";
import type { Prisma, PrismaClient } from "./client.js";
/** Group turns use channel inputs and their own outputs, never private thread history. */
export function loadRunHistoryMessages(
prisma: PrismaClient,
run: { id: string; threadId: string },
limit: number,
channelId?: string,
) {
return prisma.message.findMany({
where: {
threadId: run.threadId,
...(channelId
? {
OR: [
{
role: "user",
blocks: { array_contains: [{ kind: "channel_message", channelId }] },
},
{ role: "bot", runId: run.id },
],
}
: {}),
},
orderBy: { seq: "desc" },
take: limit,
select: {
id: true,
threadId: true,
seq: true,
role: true,
runId: true,
blocks: true,
replyToMessageId: true,
replyQuote: true,
replyTo: { select: { id: true, threadId: true, role: true, blocks: true } },
},
});
}
export interface CreateThreadMessageInput {
threadId: string;
role: "user" | "bot" | "system";
blocks: MessageBlock[];
botId?: string;
replyToMessageId?: string;
replyQuote?: string;
runId?: string;
clientNonce?: string;
markUnread?: boolean;
}
export async function createThreadMessage(prisma: PrismaClient, input: CreateThreadMessageInput) {
return prisma.$transaction((tx: Prisma.TransactionClient) =>
createThreadMessageInTransaction(tx, input),
);
}
export async function createThreadMessageInTransaction(
tx: Prisma.TransactionClient,
input: CreateThreadMessageInput,
) {
const thread = await tx.thread.update({
where: { id: input.threadId },
data: {
nextMessageSeq: { increment: 1 },
unread: (input.markUnread ?? input.role === "bot") ? true : undefined,
},
select: { nextMessageSeq: true },
});
await assertRunCanWriteHistory(tx, input.runId);
return tx.message.create({
data: {
threadId: input.threadId,
seq: thread.nextMessageSeq - 1,
role: input.role,
blocks: input.blocks as Prisma.InputJsonValue,
botId: input.botId,
replyToMessageId: input.replyToMessageId,
replyQuote: input.replyQuote,
runId: input.runId,
clientNonce: input.clientNonce,
},
});
}
export class RunHistoryWriteError extends Error {
constructor() {
super("Run cannot write thread history");
this.name = "RunHistoryWriteError";
}
}
export async function assertRunCanWriteHistory(
tx: Prisma.TransactionClient,
runId?: string,
): Promise<{ status: string; startedAt: Date | null } | undefined> {
if (!runId) return;
const run = await tx.run.findUnique({
where: { id: runId },
select: { status: true, startedAt: true },
});
if (!run || run.status === "cancelled") {
throw new RunHistoryWriteError();
}
return run;
}