When a delegated bot finishes but its wake message was deleted (e.g. thread clear), returnBotMessageOutcome used to mark botOutcomeReturnedAt and drop the coordinator reply silently. Retry instead, and block clearThread while completed bot_message runs still owe a return. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
import type { Actor } from "@rakazo/contracts";
|
|
import type { PrismaClient } from "./client.js";
|
|
|
|
export class IsolationError extends Error {
|
|
constructor(message = "Resource not found") {
|
|
super(message);
|
|
this.name = "IsolationError";
|
|
}
|
|
}
|
|
|
|
/** Unique (spaceId, userId, name) collision when renaming a bot section. */
|
|
export class BotSectionNameConflictError extends Error {
|
|
constructor(message = "Section name already used") {
|
|
super(message);
|
|
this.name = "BotSectionNameConflictError";
|
|
}
|
|
}
|
|
|
|
/** Thread clear blocked while a delegated bot run still owes its coordinator a return. */
|
|
export class ClearThreadPendingDelegatedReturnsError extends Error {
|
|
constructor(message = "Delegated bot work is still finishing") {
|
|
super(message);
|
|
this.name = "ClearThreadPendingDelegatedReturnsError";
|
|
}
|
|
}
|
|
|
|
export async function requireMembership(
|
|
prisma: PrismaClient,
|
|
userId: string,
|
|
requestedSpaceId?: string | null,
|
|
): Promise<Actor> {
|
|
const membership = await prisma.spaceMember.findFirst({
|
|
where: {
|
|
userId,
|
|
...(requestedSpaceId ? { spaceId: requestedSpaceId } : {}),
|
|
},
|
|
orderBy: [{ space: { isDefault: "desc" } }, { createdAt: "asc" }, { id: "asc" }],
|
|
include: { member: { include: { user: true } } },
|
|
});
|
|
if (!membership) {
|
|
throw new IsolationError("No personal space");
|
|
}
|
|
const settings = await prisma.deploymentSettings.findUnique({
|
|
where: { id: "default" },
|
|
});
|
|
return {
|
|
userId: membership.userId,
|
|
spaceId: membership.spaceId,
|
|
email: membership.member.user.email,
|
|
isDeploymentOwner: settings?.ownerUserId === membership.userId,
|
|
};
|
|
}
|
|
|
|
export function scoped<T extends { spaceId: string; userId?: string }>(
|
|
actor: Actor,
|
|
record: T | null,
|
|
): T {
|
|
if (!record || record.spaceId !== actor.spaceId) {
|
|
throw new IsolationError();
|
|
}
|
|
if (record.userId && record.userId !== actor.userId) {
|
|
throw new IsolationError();
|
|
}
|
|
return record;
|
|
}
|