An autopilot per thread continues the user's order run after run, with no time limit. Each run reports a checkpoint; after three runs without progress or three failed runs it pauses with Continue and Stop. Start, pause, stop and finish show as quiet lines in the conversation. Approvals during an autopilot run are parked as suggestions, and the temporary approval rules end with the autopilot. Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1 Co-authored-by: Claude <noreply@anthropic.com>
76 lines
3.4 KiB
TypeScript
76 lines
3.4 KiB
TypeScript
import type { MessageBlock, RoutineEventBlock } from "@rakazo/contracts";
|
|
import { formatCron, isOneShotRoutineCrons } from "./cron.js";
|
|
|
|
/** A message that is only timeline lines, shown without a speaker, bubble or reactions. */
|
|
export function isRoutineEventMessage(blocks: readonly MessageBlock[]): boolean {
|
|
return (
|
|
blocks.length > 0 &&
|
|
blocks.every((block) => block.kind === "event" || block.kind === "autopilot")
|
|
);
|
|
}
|
|
|
|
/** The blocks with every line about `routineId` marked removed, or null when nothing changes. */
|
|
export function markRoutineEventsRemoved(
|
|
blocks: readonly MessageBlock[],
|
|
routineId: string,
|
|
): MessageBlock[] | null {
|
|
let changed = false;
|
|
const next = blocks.map((block) => {
|
|
if (block.kind !== "event" || block.routineId !== routineId || block.removed) return block;
|
|
changed = true;
|
|
return { ...block, removed: true };
|
|
});
|
|
return changed ? next : null;
|
|
}
|
|
|
|
/** How a later turn reads the line, so the bot knows what it set up and what is gone. */
|
|
export function routineEventHistoryText(block: RoutineEventBlock): string {
|
|
if (block.removed) return `[routine removed: ${block.name}]`;
|
|
const verb = block.event === "routine.updated" ? "updated" : "set up";
|
|
const schedule = isOneShotRoutineCrons(block.crons)
|
|
? `once at ${block.nextRunAt ?? "an unknown time"}`
|
|
: `${block.crons.map(formatCron).join(", ")}, ${block.timezone}`;
|
|
return `[routine ${verb}: ${block.name} (${schedule})]`;
|
|
}
|
|
|
|
/** The parts of a routine schedule a one-line summary needs; clients word it in their locale. */
|
|
export type RoutineScheduleShape =
|
|
| { kind: "once" }
|
|
| { kind: "hourly" }
|
|
| { kind: "minutes" | "hours"; every: number }
|
|
| { kind: "daily" | "weekdays"; hour: number; minute: number }
|
|
| { kind: "weekly"; weekday: number; hour: number; minute: number }
|
|
| { kind: "monthly"; day: number; hour: number; minute: number }
|
|
| { kind: "other" };
|
|
|
|
const CLOCK_FIELD = /^\d{1,2}$/;
|
|
|
|
export function routineScheduleShape(crons: readonly string[]): RoutineScheduleShape {
|
|
if (isOneShotRoutineCrons([...crons])) return { kind: "once" };
|
|
if (crons.length !== 1) return { kind: "other" };
|
|
const parts = crons[0]!.trim().split(/\s+/);
|
|
if (parts.length !== 5) return { kind: "other" };
|
|
const [minute, hour, day, month, weekday] = parts as [string, string, string, string, string];
|
|
if (month !== "*") return { kind: "other" };
|
|
const step = (field: string) => /^\*\/(\d+)$/.exec(field)?.[1];
|
|
if (day === "*" && weekday === "*") {
|
|
if (minute === "0" && hour === "*") return { kind: "hourly" };
|
|
const minuteStep = step(minute);
|
|
if (minuteStep && hour === "*") return { kind: "minutes", every: Number(minuteStep) };
|
|
const hourStep = step(hour);
|
|
if (minute === "0" && hourStep) return { kind: "hours", every: Number(hourStep) };
|
|
}
|
|
if (!CLOCK_FIELD.test(minute) || !CLOCK_FIELD.test(hour)) return { kind: "other" };
|
|
const clock = { hour: Number(hour), minute: Number(minute) };
|
|
if (clock.hour > 23 || clock.minute > 59) return { kind: "other" };
|
|
if (day === "*" && weekday === "*") return { kind: "daily", ...clock };
|
|
if (day === "*" && weekday === "1-5") return { kind: "weekdays", ...clock };
|
|
if (day === "*" && /^[0-7]$/.test(weekday)) {
|
|
return { kind: "weekly", weekday: Number(weekday) % 7, ...clock };
|
|
}
|
|
if (weekday === "*" && /^([1-9]|[12]\d|3[01])$/.test(day)) {
|
|
return { kind: "monthly", day: Number(day), ...clock };
|
|
}
|
|
return { kind: "other" };
|
|
}
|