Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Catching up with main takes seconds when the two sides touched different files | 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 | } |