evOo-local/packages/db/src/routine-notices.ts
cynarAI dbf37f86a8
feat: show routines the bot sets up from chat as one quiet line (#133)
* feat: show routines the bot sets up from chat as one quiet line

When someone mentions something recurring in passing ("every Sunday I plan the
week"), the bot now creates the routine right away and the conversation shows a
single line with the schedule, Edit and Undo, on web and mobile.

- New `event` message block for routine lines, kept in history so later turns
  know what was set up or removed.
- schedule_create reuses an active repeating routine of the same name instead
  of adding a duplicate, caps a bot at 50 active schedules, and falls back to
  default times when none was given.
- Removing a routine (Undo, the editor, or schedule_cancel) marks its lines
  removed via the new routines.noticeMessageId column.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1

* test: give the routine notice test a complete job publisher

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-04 20:36:53 +02:00

85 lines
3.1 KiB
TypeScript

import type { RoutineEventBlock } from "@rakazo/contracts";
import { markRoutineEventsRemoved } from "@rakazo/core";
import type { Prisma, PrismaClient } from "./client.js";
import { appendEventInTransaction } from "./events.js";
import { createThreadMessageInTransaction } from "./messages.js";
export interface RoutineNoticeInput {
spaceId: string;
threadId: string;
/** The bot that set the routine up and writes the line. */
botId: string;
block: RoutineEventBlock;
}
/** Write the timeline line for a routine the bot set up and remember it on the routine. */
export async function postRoutineNotice(
prisma: PrismaClient,
input: RoutineNoticeInput,
): Promise<{ messageId: string; seq: number }> {
return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
const message = await createThreadMessageInTransaction(tx, {
threadId: input.threadId,
role: "bot",
blocks: [input.block],
botId: input.botId,
markUnread: false,
});
await tx.routine.updateMany({
where: { id: input.block.routineId },
data: { noticeMessageId: message.id },
});
const event = await appendEventInTransaction(tx, {
spaceId: input.spaceId,
threadId: input.threadId,
botId: input.botId,
type: "thread.message.created",
payload: { messageId: message.id, role: "bot", blocks: [input.block] },
});
return { messageId: message.id, seq: event.seq };
});
}
/**
* Mark every line about a removed routine in the thread of its latest line. Returns the thread and
* event seq to notify, or null when the routine had no line or nothing changed.
*/
export async function markRoutineNoticesRemoved(
prisma: PrismaClient,
routine: { id: string; botId: string; noticeMessageId: string | null },
): Promise<{ threadId: string; seq: number } | null> {
if (!routine.noticeMessageId) return null;
return prisma.$transaction(async (tx: Prisma.TransactionClient) => {
const notice = await tx.message.findUnique({
where: { id: routine.noticeMessageId! },
select: { threadId: true, thread: { select: { spaceId: true } } },
});
if (!notice) return null;
const messages = await tx.message.findMany({
where: {
threadId: notice.threadId,
blocks: { array_contains: [{ kind: "event", routineId: routine.id }] },
},
select: { id: true, role: true, botId: true, blocks: true },
});
let seq: number | null = null;
for (const message of messages) {
const blocks = markRoutineEventsRemoved(message.blocks as RoutineEventBlock[], routine.id);
if (!blocks) continue;
await tx.message.update({
where: { id: message.id },
data: { blocks: blocks as Prisma.InputJsonValue },
});
const event = await appendEventInTransaction(tx, {
spaceId: notice.thread.spaceId,
threadId: notice.threadId,
botId: message.botId ?? routine.botId,
type: "thread.message.updated",
payload: { messageId: message.id, role: message.role, blocks },
});
seq = event.seq;
}
return seq === null ? null : { threadId: notice.threadId, seq };
});
}