export type LoopLimitConfig = | { kind: "iterations"; iterations: number; } | { kind: "duration"; durationMs: number; }; export type LoopLimitRuntime = | { kind: "iterations"; initial: number; remaining: number; } | { kind: "duration"; durationMs: number; deadlineMs: number; }; const TIME_UNITS_MS = new Map([ ["s", 1_000], ["sec", 1_000], ["secs", 1_000], ["second", 1_000], ["seconds", 1_000], ["m", 60_000], ["min", 60_000], ["mins", 60_000], ["minute", 60_000], ["minutes", 60_000], ["h", 3_600_000], ["hr", 3_600_000], ["hrs", 3_600_000], ["hour", 3_600_000], ["hours", 3_600_000], ]); export function parseLoopLimitArgs(args: string): LoopLimitConfig | undefined | string { const trimmed = args.trim().toLowerCase(); if (!trimmed) return undefined; const parts = trimmed.split(/\s+/); if (parts.length > 2) { return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min."; } if (parts.length === 2) { return parseDurationParts(parts[0], parts[1]); } const token = parts[0]; const iterationMatch = /^(\d+)$/.exec(token); if (iterationMatch) { const iterations = Number(iterationMatch[1]); if (!Number.isSafeInteger(iterations) || iterations <= 0) { return "Loop count must be a positive integer."; } return { kind: "iterations", iterations }; } const durationMatch = /^(\d+)([a-z]+)$/.exec(token); if (durationMatch) { return parseDurationParts(durationMatch[1], durationMatch[2]); } return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min."; } function parseDurationParts(amountText: string, unitText: string): LoopLimitConfig | string { if (!/^\d+$/.test(amountText)) { return "Loop duration must use a positive integer amount."; } const amount = Number(amountText); if (!Number.isSafeInteger(amount) || amount <= 0) { return "Loop duration must be positive."; } const unitMs = TIME_UNITS_MS.get(unitText); if (unitMs === undefined) { return "Loop duration unit must be seconds, minutes, or hours."; } return { kind: "duration", durationMs: amount * unitMs }; } export function createLoopLimitRuntime( config: LoopLimitConfig | undefined, nowMs = Date.now(), ): LoopLimitRuntime | undefined { if (!config) return undefined; if (config.kind === "iterations") { return { kind: "iterations", initial: config.iterations, remaining: config.iterations }; } return { kind: "duration", durationMs: config.durationMs, deadlineMs: nowMs + config.durationMs }; } export function consumeLoopLimitIteration(limit: LoopLimitRuntime | undefined, nowMs = Date.now()): boolean { if (!limit) return true; if (limit.kind === "duration") { return nowMs < limit.deadlineMs; } if (limit.remaining <= 0) return false; limit.remaining -= 1; return true; } export function isLoopDurationExpired(limit: LoopLimitRuntime | undefined, nowMs = Date.now()): boolean { return limit?.kind === "duration" && nowMs >= limit.deadlineMs; } export function describeLoopLimit(config: LoopLimitConfig): string { if (config.kind === "iterations") { return `${config.iterations} ${config.iterations === 1 ? "iteration" : "iterations"}`; } return formatDuration(config.durationMs); } export function describeLoopLimitRuntime(limit: LoopLimitRuntime): string { if (limit.kind === "iterations") { return `${limit.remaining} of ${limit.initial} ${limit.initial === 1 ? "iteration" : "iterations"} remaining`; } return `${formatDuration(limit.durationMs)} limit`; } function formatDuration(durationMs: number): string { if (durationMs % 3_600_000 === 0) { const hours = durationMs / 3_600_000; return `${hours} ${hours === 1 ? "hour" : "hours"}`; } if (durationMs % 60_000 === 0) { const minutes = durationMs / 60_000; return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`; } const seconds = durationMs / 1_000; return `${seconds} ${seconds === 1 ? "second" : "seconds"}`; }