136 lines · 5.4 KB · TypeScript
RawDownload
import type { OrchestrationThreadShell } from "@t3tools/contracts";import { visibleThreadPullRequests } from "@t3tools/shared/threadPullRequests"; export interface SettlementPullRequest {  readonly state: "open" | "closed" | "merged";  readonly closedAt?: string | null;  readonly mergedAt?: string | null;  readonly updatedAt?: string | null;} const DAY_MS = 24 * 60 * 60 * 1_000;const QUEUED_TURN_START_GRACE_MS = 2 * 60 * 1_000; function latestTimestamp(values: ReadonlyArray<string | null | undefined>): string | null {  let latest: string | null = null;  let latestMs = Number.NEGATIVE_INFINITY;  for (const value of values) {    if (value == null) continue;    const valueMs = Date.parse(value);    if (valueMs > latestMs) {      latest = value;      latestMs = valueMs;    }  }  return latest;} /** A recent user message stays queued until a turn adopts its timestamp. * Absolute age bounds client clock skew in both directions and stops stale * pre-adoption data from blocking the thread forever. */export function threadHasQueuedTurnStart(  thread: Pick<OrchestrationThreadShell, "latestUserMessageAt" | "latestTurn" | "session">,  now: string,): boolean {  if (thread.latestUserMessageAt === null || thread.session?.status === "error") return false;  const messageAt = Date.parse(thread.latestUserMessageAt);  const age = Date.parse(now) - messageAt;  if (Number.isNaN(age) || Math.abs(age) > QUEUED_TURN_START_GRACE_MS) return false;  if (thread.latestTurn === null) return true;  return [    thread.latestTurn.requestedAt,    thread.latestTurn.startedAt,    thread.latestTurn.completedAt,  ].every((value) => value == null || Date.parse(value) < messageAt);} function pullRequestSettles(  thread: Pick<OrchestrationThreadShell, "createdAt" | "latestUserMessageAt" | "latestTurn">,  pullRequest: SettlementPullRequest,  autoSettleOnMerge: boolean,): boolean {  if (pullRequest.state !== "closed" && (pullRequest.state !== "merged" || !autoSettleOnMerge)) {    return false;  }  const terminalAt = pullRequest.state === "merged" ? pullRequest.mergedAt : pullRequest.closedAt;  if (terminalAt == null) return false;  const userAnchor = latestTimestamp([    thread.createdAt,    thread.latestUserMessageAt,    thread.latestTurn?.requestedAt,  ]);  if (userAnchor === null) return false;  const pullRequestAt = Date.parse(terminalAt);  const userAnchorAt = Date.parse(userAnchor);  if (Number.isNaN(pullRequestAt) || Number.isNaN(userAnchorAt)) return false;  return pullRequestAt >= userAnchorAt;} export function resolveAutoSettlementAt(input: {  readonly thread: OrchestrationThreadShell;  readonly pullRequest: SettlementPullRequest | null;  readonly now: string;  readonly autoSettleAfterDays: number | null;  readonly autoSettleOnMerge: boolean;}): string | null {  const { thread } = input;  let pullRequest = input.pullRequest;  const links = visibleThreadPullRequests(thread.pullRequests);  if (links.some((link) => link.snapshot === null || link.snapshot.state === "open")) return null;  if (links.length > 0) {    const terminalTimestamp = (link: (typeof links)[number]) => {      const snapshot = link.snapshot;      const value = snapshot?.state === "merged" ? snapshot.mergedAt : snapshot?.closedAt;      const timestamp = Date.parse(value ?? "");      return Number.isNaN(timestamp) ? Number.NEGATIVE_INFINITY : timestamp;    };    const latest = links.reduce((current, candidate) =>      terminalTimestamp(candidate) > terminalTimestamp(current) ? candidate : current,    );    pullRequest =      latest.snapshot === null        ? null        : {            state: latest.snapshot.state,            mergedAt: latest.snapshot.mergedAt ?? null,            closedAt: latest.snapshot.closedAt ?? null,          };  }  if (!isAutoSettlementCandidate(thread, input.now)) return null;  const activityAt = latestTimestamp([    thread.latestUserMessageAt,    thread.latestTurn?.requestedAt,    thread.latestTurn?.startedAt,    thread.latestTurn?.completedAt,  ]);  if (pullRequest !== null) {    if (pullRequestSettles(thread, pullRequest, input.autoSettleOnMerge)) {      return activityAt ?? thread.createdAt;    }  }  if (input.autoSettleAfterDays === null || activityAt === null) return null;  return Date.parse(activityAt) < Date.parse(input.now) - input.autoSettleAfterDays * DAY_MS    ? activityAt    : null;} /** Cheap checks that run before any source control lookup. */export function isAutoSettlementCandidate(thread: OrchestrationThreadShell, now: string): boolean {  if (thread.archivedAt !== null || thread.settledOverride !== null) return false;  if (thread.hasPendingApprovals || thread.hasPendingUserInput) return false;  if (thread.session?.status === "starting" || thread.session?.status === "running") return false;  if (thread.backgroundLiveness != null) return false;  if (threadHasQueuedTurnStart(thread, now)) return false;  if (thread.snoozedUntil == null || Date.parse(thread.snoozedUntil) <= Date.parse(now))    return true;  const wokeOnError =    thread.session?.status === "error" &&    (thread.snoozedAt == null ||      Date.parse(thread.session.updatedAt) > Date.parse(thread.snoozedAt));  const wokeOnCompletion =    thread.snoozedAt != null &&    thread.latestTurn?.state === "completed" &&    thread.latestTurn.completedAt != null &&    Date.parse(thread.latestTurn.completedAt) > Date.parse(thread.snoozedAt);  return wokeOnError || wokeOnCompletion;}