g1t/apps/web/app/lib/catch-up.ts
| 1 | /** |
| 2 | * Where a pull request's catch-up stands once it has been handed to a |
| 3 | * sandbox: still going, done, failed, or taking far longer than it should. |
| 4 | * Pure, so it is tested on its own; it imports only types. |
| 5 | */ |
| 6 | import type { AgentRun, PullBranchUpdate } from "@g1t/contracts"; |
| 7 | |
| 8 | /** A sandbox catch-up usually takes about a minute; past this, say so. */ |
| 9 | export const CATCH_UP_TIMEOUT_MS = 5 * 60_000; |
| 10 | /** Runs created this long before the request was made still count as its run. */ |
| 11 | const CLOCK_SLACK_MS = 30_000; |
| 12 | |
| 13 | export type CatchUpPhase = "working" | "done" | "failed" | "timed_out"; |
| 14 | |
| 15 | /** |
| 16 | * The `update` run started for a request made at `startedAt`, newest |
| 17 | * first, if it has shown up yet. |
| 18 | */ |
| 19 | export function catchUpRun(runs: AgentRun[], startedAt: number): AgentRun | null { |
| 20 | return ( |
| 21 | runs |
| 22 | .filter((run) => run.kind === "update" && Date.parse(run.createdAt) >= startedAt - CLOCK_SLACK_MS) |
| 23 | .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))[0] ?? null |
| 24 | ); |
| 25 | } |
| 26 | |
| 27 | /** |
| 28 | * Where it stands. It is done as soon as the pull request is no longer |
| 29 | * behind, whatever the run says; failed when its run ended without that; |
| 30 | * timed out when nothing has settled it for too long. |
| 31 | */ |
| 32 | export function catchUpPhase({ |
| 33 | behind, |
| 34 | run, |
| 35 | startedAt, |
| 36 | now, |
| 37 | }: { |
| 38 | behind: boolean; |
| 39 | run: AgentRun | null; |
| 40 | startedAt: number; |
| 41 | now: number; |
| 42 | }): CatchUpPhase { |
| 43 | if (!behind) return "done"; |
| 44 | if (run && (run.status === "failed" || run.status === "stopped")) return "failed"; |
| 45 | // Succeeded, but the page has not seen the push yet: give it the same time. |
| 46 | if (now - startedAt > CATCH_UP_TIMEOUT_MS) return "timed_out"; |
| 47 | return "working"; |
| 48 | } |
| 49 | |
| 50 | type NeedsAgent = Extract<PullBranchUpdate, { outcome: "needs_agent" }>; |
| 51 | |
| 52 | /** What the box says while a sandbox brings the pull request up to date. */ |
| 53 | export function catchUpTitle(reason: NeedsAgent["reason"], defaultBranch: string): string { |
| 54 | return reason === "conflicting" |
| 55 | ? `g1t-agent is resolving conflicts with ${defaultBranch}` |
| 56 | : `g1t-agent is merging ${defaultBranch} into this pull request`; |
| 57 | } |
| 58 | |
| 59 | /** Why it went to a sandbox, in a sentence. */ |
| 60 | export function catchUpWhy(update: Pick<NeedsAgent, "reason" | "paths">, defaultBranch: string): string { |
| 61 | const files = update.paths.length === 1 ? "one file" : `${update.paths.length} files`; |
| 62 | switch (update.reason) { |
| 63 | case "conflicting": |
| 64 | return `Merging ${defaultBranch} conflicts in ${files}, so g1t-agent resolves them in a sandbox and pushes the result.`; |
| 65 | case "overlap": |
| 66 | return update.paths.length > 0 |
| 67 | ? `This pull request and ${defaultBranch} both changed ${files}, so they are merged with git in a sandbox. g1t-agent resolves any conflicts.` |
| 68 | : `They are merged with git in a sandbox. g1t-agent resolves any conflicts.`; |
| 69 | default: |
| 70 | return `They are merged with git in a sandbox. g1t-agent resolves any conflicts.`; |
| 71 | } |
| 72 | } |