From 7fe088aef9c75455ce70967e6d489f7d131fbc60 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 7 May 2026 13:51:42 -0500 Subject: [PATCH 1/3] feat(sandbox): port org base-snapshot lookup from open-agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the largest user-visible regression in the cutover gap analysis: ~75s slower cold start per session because api wasn't reading the per-org base snapshots open-agents builds. After this, api warm-boots recoupable org sessions from the most recent created snapshot when one exists, falling through to default provisioning otherwise. Scope: lookup only. - `extractOrgRepoName(repoUrl)` matches `https://github.com/recoupable/X` and returns X (or null for non-recoupable repos) - `findOrgSnapshot(name)` calls `Snapshot.list({ name, sortOrder: "desc", limit: 5 })` from `@vercel/sandbox` and returns the first `created` snapshot's id, null on miss / error - `createSandboxHandler` runs the lookup only when extractOrgRepoName returns non-null (skips for non-recoupable repos so the latency cost only applies where it can pay off), then plumbs the resolved id into `connectSandbox` options as `baseSnapshotId` Out of scope (will need its own PR): - `kickBuildOrgSnapshotWorkflow` — builds new snapshots when none exist yet. Open-agents currently does this via Vercel Workflow. Skipped here because (a) api doesn't have Vercel Workflow infra yet, and (b) open-agents' workflow keeps building snapshots today, so api can immediately benefit by reading what open-agents writes. Once open-agents is fully cut over to api, the build piece needs to land too — at that point new orgs would never get a snapshot. TDD red -> green: - 7 unit tests for extractOrgRepoName (recoupable URL, .git suffix, trailing slash, non-recoupable orgs, nested paths, non-GitHub, org-root-no-repo) - 5 unit tests for findOrgSnapshot (most-recent-created, list call shape, no-created-state, empty list, throw) - 3 new createSandboxHandler tests (recoupable repo + lookup hit plumbs baseSnapshotId, non-recoupable repo skips lookup entirely, recoupable repo + lookup miss does not pass baseSnapshotId) - Suite: 2559 -> 2574 (+15 tests). pnpm lint:check + tsc --noEmit clean for new files. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/extractOrgRepoName.test.ts | 36 ++++++++++++ lib/recoupable/extractOrgRepoName.ts | 19 +++++++ .../__tests__/createSandboxHandler.test.ts | 57 +++++++++++++++++++ lib/sandbox/__tests__/findOrgSnapshot.test.ts | 55 ++++++++++++++++++ lib/sandbox/createSandboxHandler.ts | 11 ++++ lib/sandbox/findOrgSnapshot.ts | 29 ++++++++++ 6 files changed, 207 insertions(+) create mode 100644 lib/recoupable/__tests__/extractOrgRepoName.test.ts create mode 100644 lib/recoupable/extractOrgRepoName.ts create mode 100644 lib/sandbox/__tests__/findOrgSnapshot.test.ts create mode 100644 lib/sandbox/findOrgSnapshot.ts diff --git a/lib/recoupable/__tests__/extractOrgRepoName.test.ts b/lib/recoupable/__tests__/extractOrgRepoName.test.ts new file mode 100644 index 00000000..8f5b2c9a --- /dev/null +++ b/lib/recoupable/__tests__/extractOrgRepoName.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { extractOrgRepoName } from "@/lib/recoupable/extractOrgRepoName"; + +describe("extractOrgRepoName", () => { + it("extracts the repo name from a recoupable org clone URL", () => { + expect(extractOrgRepoName("https://github.com/recoupable/org-rostrum-pacific-abc123")).toBe( + "org-rostrum-pacific-abc123", + ); + }); + + it("strips a trailing .git suffix", () => { + expect(extractOrgRepoName("https://github.com/recoupable/myorg.git")).toBe("myorg"); + }); + + it("strips a trailing slash", () => { + expect(extractOrgRepoName("https://github.com/recoupable/myorg/")).toBe("myorg"); + }); + + it("returns null for non-recoupable orgs", () => { + expect(extractOrgRepoName("https://github.com/someoneelse/repo")).toBeNull(); + }); + + it("returns null for nested paths beyond the repo segment", () => { + expect(extractOrgRepoName("https://github.com/recoupable/repo/blob/main/x")).toBeNull(); + }); + + it("returns null for non-GitHub URLs", () => { + expect(extractOrgRepoName("https://gitlab.com/recoupable/repo")).toBeNull(); + expect(extractOrgRepoName("not-a-url")).toBeNull(); + }); + + it("returns null for the org root with no repo", () => { + expect(extractOrgRepoName("https://github.com/recoupable/")).toBeNull(); + expect(extractOrgRepoName("https://github.com/recoupable")).toBeNull(); + }); +}); diff --git a/lib/recoupable/extractOrgRepoName.ts b/lib/recoupable/extractOrgRepoName.ts new file mode 100644 index 00000000..a033fdb6 --- /dev/null +++ b/lib/recoupable/extractOrgRepoName.ts @@ -0,0 +1,19 @@ +const ORG_REPO_URL_PATTERN = /^https:\/\/github\.com\/recoupable\/([^/]+?)(?:\.git)?\/?$/; + +/** + * Extracts the repo name from a Recoupable org clone URL. The repo + * name is used as a `sandboxName` to look up per-org base snapshots + * built by the build-org-snapshot workflow — finding one warm-boots + * the sandbox in seconds instead of paying the ~75s full-clone path. + * + * Example: `https://github.com/recoupable/org-rostrum-pacific-` + * → `org-rostrum-pacific-` + * + * @param cloneUrl - The repo URL the caller wants to clone. + * @returns The repo name when the URL is under the recoupable org, + * otherwise null. Non-recoupable repos skip the snapshot lookup. + */ +export function extractOrgRepoName(cloneUrl: string): string | null { + const match = cloneUrl.match(ORG_REPO_URL_PATTERN); + return match?.[1] ?? null; +} diff --git a/lib/sandbox/__tests__/createSandboxHandler.test.ts b/lib/sandbox/__tests__/createSandboxHandler.test.ts index d257f9b3..a11aeadb 100644 --- a/lib/sandbox/__tests__/createSandboxHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxHandler.test.ts @@ -7,6 +7,7 @@ import { selectSessions } from "@/lib/supabase/sessions/selectSessions"; import { connectSandbox } from "@/lib/sandbox/factory"; import { updateSession } from "@/lib/supabase/sessions/updateSession"; import { installSessionGlobalSkills } from "@/lib/sandbox/installSessionGlobalSkills"; +import { findOrgSnapshot } from "@/lib/sandbox/findOrgSnapshot"; vi.mock("@/lib/networking/getCorsHeaders", () => ({ getCorsHeaders: () => ({ "Access-Control-Allow-Origin": "*" }), @@ -29,6 +30,9 @@ vi.mock("@/lib/github/getServiceGithubToken", () => ({ vi.mock("@/lib/sandbox/installSessionGlobalSkills", () => ({ installSessionGlobalSkills: vi.fn(async () => undefined), })); +vi.mock("@/lib/sandbox/findOrgSnapshot", () => ({ + findOrgSnapshot: vi.fn(async () => null), +})); const ACCOUNT_ID = "acc-1"; @@ -167,6 +171,59 @@ describe("createSandboxHandler", () => { expect(res.status).toBe(200); }); + it("looks up an org snapshot and plumbs its id into baseSnapshotId when the repo is a recoupable org repo", async () => { + vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({ + body: { + repoUrl: "https://github.com/recoupable/org-acme-xyz", + sessionId: "sess-1", + }, + auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" }, + }); + vi.mocked(findOrgSnapshot).mockResolvedValueOnce("snap_abc123"); + + await createSandboxHandler(makeReq()); + + expect(findOrgSnapshot).toHaveBeenCalledWith("org-acme-xyz"); + const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0]; + if (!arg || !("options" in arg)) throw new Error("expected new-API config shape"); + expect(arg.options?.baseSnapshotId).toBe("snap_abc123"); + }); + + it("skips the snapshot lookup entirely for non-recoupable repos", async () => { + vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({ + body: { + repoUrl: "https://github.com/someoneelse/repo", + sessionId: "sess-1", + }, + auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" }, + }); + + await createSandboxHandler(makeReq()); + + expect(findOrgSnapshot).not.toHaveBeenCalled(); + const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0]; + if (!arg || !("options" in arg)) throw new Error("expected new-API config shape"); + expect(arg.options?.baseSnapshotId).toBeUndefined(); + }); + + it("does not pass baseSnapshotId when the org snapshot lookup misses", async () => { + vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({ + body: { + repoUrl: "https://github.com/recoupable/org-no-snap-yet", + sessionId: "sess-1", + }, + auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" }, + }); + vi.mocked(findOrgSnapshot).mockResolvedValueOnce(null); + + await createSandboxHandler(makeReq()); + + expect(findOrgSnapshot).toHaveBeenCalledWith("org-no-snap-yet"); + const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0]; + if (!arg || !("options" in arg)) throw new Error("expected new-API config shape"); + expect(arg.options?.baseSnapshotId).toBeUndefined(); + }); + it("does not attempt skill installation when no sessionId is provided", async () => { vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({ body: { repoUrl: "https://github.com/o/r" }, diff --git a/lib/sandbox/__tests__/findOrgSnapshot.test.ts b/lib/sandbox/__tests__/findOrgSnapshot.test.ts new file mode 100644 index 00000000..bd992310 --- /dev/null +++ b/lib/sandbox/__tests__/findOrgSnapshot.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { findOrgSnapshot } from "@/lib/sandbox/findOrgSnapshot"; +import { Snapshot } from "@vercel/sandbox"; + +vi.mock("@vercel/sandbox", () => ({ + Snapshot: { list: vi.fn() }, +})); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("findOrgSnapshot", () => { + it("returns the id of the most recent created snapshot", async () => { + vi.mocked(Snapshot.list).mockResolvedValue({ + snapshots: [ + { id: "snap_A", status: "creating" }, + { id: "snap_B", status: "created" }, + { id: "snap_C", status: "created" }, + ], + } as never); + + const id = await findOrgSnapshot("org-x"); + // Returns the FIRST created snapshot in the list (sortOrder desc means first = newest). + expect(id).toBe("snap_B"); + }); + + it("calls Snapshot.list with the supplied name and a desc sort order", async () => { + vi.mocked(Snapshot.list).mockResolvedValue({ snapshots: [] } as never); + + await findOrgSnapshot("org-y"); + + expect(Snapshot.list).toHaveBeenCalledWith( + expect.objectContaining({ name: "org-y", sortOrder: "desc" }), + ); + }); + + it("returns null when no snapshots are in the 'created' state", async () => { + vi.mocked(Snapshot.list).mockResolvedValue({ + snapshots: [{ id: "snap_pending", status: "creating" }], + } as never); + + expect(await findOrgSnapshot("org-z")).toBeNull(); + }); + + it("returns null when the API returns no snapshots", async () => { + vi.mocked(Snapshot.list).mockResolvedValue({ snapshots: [] } as never); + expect(await findOrgSnapshot("org-empty")).toBeNull(); + }); + + it("returns null when Snapshot.list throws", async () => { + vi.mocked(Snapshot.list).mockRejectedValue(new Error("vercel api down")); + expect(await findOrgSnapshot("org-err")).toBeNull(); + }); +}); diff --git a/lib/sandbox/createSandboxHandler.ts b/lib/sandbox/createSandboxHandler.ts index 00891e4c..45a51752 100644 --- a/lib/sandbox/createSandboxHandler.ts +++ b/lib/sandbox/createSandboxHandler.ts @@ -4,8 +4,10 @@ import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { validateCreateSandboxBody } from "@/lib/sandbox/validateCreateSandboxBody"; import { selectSessions } from "@/lib/supabase/sessions/selectSessions"; import { connectSandbox } from "@/lib/sandbox/factory"; +import { findOrgSnapshot } from "@/lib/sandbox/findOrgSnapshot"; import { getSessionSandboxName } from "@/lib/sandbox/getSessionSandboxName"; import { installSessionGlobalSkills } from "@/lib/sandbox/installSessionGlobalSkills"; +import { extractOrgRepoName } from "@/lib/recoupable/extractOrgRepoName"; import { updateSession } from "@/lib/supabase/sessions/updateSession"; import { getServiceGithubToken } from "@/lib/github/getServiceGithubToken"; import type { Json, Tables } from "@/types/database.types"; @@ -57,6 +59,14 @@ export async function createSandboxHandler(request: NextRequest): Promise { + try { + const result = await Snapshot.list({ + name: sandboxName, + sortOrder: "desc", + limit: 5, + }); + const ready = result.snapshots.find(s => s.status === "created"); + return ready?.id ?? null; + } catch (error) { + console.error(`[findOrgSnapshot] failed to list snapshots for '${sandboxName}':`, error); + return null; + } +} From 25e4b7835f5274c34dc03046fdd0224cf40a2c1f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 7 May 2026 14:23:39 -0500 Subject: [PATCH 2/3] chore(sandbox): log findOrgSnapshot outcomes for production observability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a single structured log line on the success path: [findOrgSnapshot] '' → (N total returned) Useful both for the cutover verification (proves the lookup ran for specific request URLs without needing to redeploy with debug instrumentation) and for ongoing prod observability — when an org's snapshot pipeline breaks, this is the line that surfaces it. Error path log was already present. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/sandbox/findOrgSnapshot.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/sandbox/findOrgSnapshot.ts b/lib/sandbox/findOrgSnapshot.ts index 8063bb44..59fa7e6d 100644 --- a/lib/sandbox/findOrgSnapshot.ts +++ b/lib/sandbox/findOrgSnapshot.ts @@ -21,6 +21,9 @@ export async function findOrgSnapshot(sandboxName: string): Promise s.status === "created"); + console.log( + `[findOrgSnapshot] '${sandboxName}' → ${ready ? `hit ${ready.id}` : "miss"} (${result.snapshots.length} total snapshots returned)`, + ); return ready?.id ?? null; } catch (error) { console.error(`[findOrgSnapshot] failed to list snapshots for '${sandboxName}':`, error); From 26b4be0247e53ef46a91846b3098b85fc0f1430e Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 7 May 2026 14:52:46 -0500 Subject: [PATCH 3/3] fix(sandbox): set source.prebuilt:true when restoring from org snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught during the hit-case smoke test against a real recoupable org repo: with a snapshot found and `baseSnapshotId` plumbed in, the sandbox boot still fell through to a fresh `git clone`, which then failed with exit 128. Reason: I'd dropped the `prebuilt` source flag from the port, calling it "informational." It is not. Reading lib/sandbox/vercel/sandbox/VercelSandbox.ts, the flag switches between two distinct boot paths: - `source && baseSnapshotId && !source.prebuilt` → fresh clone on top of snapshot (often fails for private repos and defeats the warm-boot benefit) - `source?.prebuilt && baseSnapshotId` → `git fetch` + `git reset --hard` against the repo that's already inside the snapshot (the fast path) Setting `prebuilt: !!orgSnapshotId` matches open-agents' behavior and unlocks the actual ~75s warm-boot win this PR exists to enable. Tests updated: existing assertions for hit-case extended to also verify `source.prebuilt === true` when a snapshot is found, and `source.prebuilt === false` when the lookup misses. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/sandbox/__tests__/createSandboxHandler.test.ts | 6 ++++++ lib/sandbox/createSandboxHandler.ts | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/sandbox/__tests__/createSandboxHandler.test.ts b/lib/sandbox/__tests__/createSandboxHandler.test.ts index a11aeadb..dbc49eff 100644 --- a/lib/sandbox/__tests__/createSandboxHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxHandler.test.ts @@ -186,7 +186,10 @@ describe("createSandboxHandler", () => { expect(findOrgSnapshot).toHaveBeenCalledWith("org-acme-xyz"); const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0]; if (!arg || !("options" in arg)) throw new Error("expected new-API config shape"); + if (!("state" in arg)) throw new Error("expected new-API state shape"); expect(arg.options?.baseSnapshotId).toBe("snap_abc123"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((arg.state as any).source.prebuilt).toBe(true); }); it("skips the snapshot lookup entirely for non-recoupable repos", async () => { @@ -221,7 +224,10 @@ describe("createSandboxHandler", () => { expect(findOrgSnapshot).toHaveBeenCalledWith("org-no-snap-yet"); const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0]; if (!arg || !("options" in arg)) throw new Error("expected new-API config shape"); + if (!("state" in arg)) throw new Error("expected new-API state shape"); expect(arg.options?.baseSnapshotId).toBeUndefined(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((arg.state as any).source.prebuilt).toBe(false); }); it("does not attempt skill installation when no sessionId is provided", async () => { diff --git a/lib/sandbox/createSandboxHandler.ts b/lib/sandbox/createSandboxHandler.ts index 45a51752..01e5f499 100644 --- a/lib/sandbox/createSandboxHandler.ts +++ b/lib/sandbox/createSandboxHandler.ts @@ -75,7 +75,13 @@ export async function createSandboxHandler(request: NextRequest): Promise