101 lines · 3.5 KB · TypeScript
RawDownload
import type {  RelayAgentActivityAggregateRow,  RelayAgentActivityAggregateState,  RelayAgentActivityState,} from "@t3tools/contracts/relay";import * as DateTime from "effect/DateTime";import * as Option from "effect/Option"; import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts"; export function isTerminalPhase(state: RelayAgentActivityState): boolean {  return state.phase === "completed" || state.phase === "failed";} // Rows are only removed when their environment publishes a terminal state. An// environment that dies mid-run (machine off, process killed) never does, so// without an age cutoff its threads inflate activeCount forever. Actively// running phases expire quickly; waiting phases can legitimately sit for hours// while a user ignores an approval prompt, so they get a longer window. The// underlying database row is left in place: a late publish for the thread// refreshes updatedAt and the row becomes visible again.const RUNNING_AGENT_ACTIVITY_ROW_TTL_MS = 2 * 60 * 60 * 1_000;const WAITING_AGENT_ACTIVITY_ROW_TTL_MS = 24 * 60 * 60 * 1_000; export function isExpiredAgentActivityState(  state: RelayAgentActivityState,  nowMs: number,): boolean {  const updatedAtMs = Option.match(DateTime.make(state.updatedAt), {    onNone: () => Number.NaN,    onSome: (dt) => dt.epochMilliseconds,  });  if (Number.isNaN(updatedAtMs)) {    return true;  }  const ttlMs =    state.phase === "running" || state.phase === "starting"      ? RUNNING_AGENT_ACTIVITY_ROW_TTL_MS      : WAITING_AGENT_ACTIVITY_ROW_TTL_MS;  return nowMs - updatedAtMs > ttlMs;} const MAX_SUMMARY_TEXT_LENGTH = 120;const MAX_STATUS_TEXT_LENGTH = 40;const MAX_DEEP_LINK_LENGTH = 512;// The Live Activity banner (lock screen / Notification Center) renders up to// five rows; the expanded Dynamic Island shows the top three of these.export const MAX_ACTIVITY_ROWS = 5; function truncateText(value: string, maxLength: number): string {  const trimmed = value.trim();  if (trimmed.length <= maxLength) {    return trimmed;  }  return trimmed.slice(0, maxLength - 3).trimEnd() + "...";} function sanitizeDeepLink(value: string): string {  const trimmed = value.trim();  if (!trimmed.startsWith("/") || trimmed.startsWith("//")) {    return "/";  }  return truncateText(trimmed, MAX_DEEP_LINK_LENGTH);} export function sanitizeAgentActivityAggregateRow(  row: RelayAgentActivityAggregateRow,): RelayAgentActivityAggregateRow {  return {    ...row,    projectTitle: truncateText(row.projectTitle, MAX_SUMMARY_TEXT_LENGTH),    threadTitle: truncateText(row.threadTitle, MAX_SUMMARY_TEXT_LENGTH),    modelTitle: truncateText(row.modelTitle, MAX_SUMMARY_TEXT_LENGTH),    status: truncateText(row.status, MAX_STATUS_TEXT_LENGTH),    deepLink: sanitizeDeepLink(row.deepLink),  };} export function sanitizeAgentActivityAggregateState(  aggregate: RelayAgentActivityAggregateState,): RelayAgentActivityAggregateState {  return {    ...aggregate,    title: truncateText(aggregate.title, MAX_SUMMARY_TEXT_LENGTH),    subtitle: truncateText(aggregate.subtitle, MAX_SUMMARY_TEXT_LENGTH),    activities: aggregate.activities      .slice(0, MAX_ACTIVITY_ROWS)      .map(sanitizeAgentActivityAggregateRow),  };} export function sanitizeApnsNotificationPayload(  notification: ApnsNotificationPayload,): ApnsNotificationPayload {  return {    ...notification,    title: truncateText(notification.title, MAX_SUMMARY_TEXT_LENGTH),    body: truncateText(notification.body, MAX_SUMMARY_TEXT_LENGTH),    deepLink: sanitizeDeepLink(notification.deepLink),  };}