Skip to content

Fix repeated Studio builds and Sandbox cleanup - #29

Merged
svg8bit merged 5 commits into
mainfrom
codex/drops-studio-v0-runtime-resume
Aug 2, 2026
Merged

Fix repeated Studio builds and Sandbox cleanup#29
svg8bit merged 5 commits into
mainfrom
codex/drops-studio-v0-runtime-resume

Conversation

@svg8bit

@svg8bit svg8bit commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • consume new-project auto-build intent exactly once so reopen, reload, and Connections return do not restart the Sandbox
  • keep the Drops Agent timeline current through Sandbox and verification phases
  • show truthful ready, building, retry, and verified states
  • align idle cleanup with stable Vercel Sandbox list limits and sorting requirements
  • refresh the explicitly approved Studio status visual baselines

Root cause

The auto-build lease was keyed to project revision and removed after each run. Revisiting Studio could therefore enqueue a fresh build. Separately, cleanup sent an unsupported page size and omitted the required name sort, so ds2 Sandbox cleanup failed before inspecting resources.

Validation

  • npm audit: 0 vulnerabilities
  • UI guardrails, ESLint, TypeScript: pass
  • unit: 799 pass, 0 fail, 0 skipped
  • focused Sandbox and build-intent tests: 18 pass
  • Storybook build/tests/visual: 47/47 and 42/42 pass
  • E2E: 160 pass plus 2 intentional visual baseline updates; targeted updated set 3/3 pass
  • Vercel and Vinext/Cloudflare builds: pass
  • CodeRabbit light review: complete; both findings fixed
  • Lighthouse: two runs inside the 300 ms TBT budget, one local noisy run at 326.8 ms; threshold unchanged
  • live cleanup: 14 ds2 Sandboxes stopped, 0 failures

Summary by CodeRabbit

  • New Features

    • Added automatic build tracking in Project Studio with building, ready, retry, blocked, and verification statuses.
    • Studio now preserves build requests during navigation and removes temporary URL parameters after use.
    • Improved automatic build handling to prevent duplicate executions and provide clearer progress feedback.
  • Bug Fixes

    • Sandbox cleanup now processes up to 50 idle sandboxes per run, with consistent name ordering.
    • Improved build readiness detection and cleanup of in-progress operations.
    • Added safer retry handling for queued or interrupted builds.

@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
drops-studio Ready Ready Preview Aug 2, 2026 1:02am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 34ecfaf9-22a7-4338-9460-da4e3f60b49e

📥 Commits

Reviewing files that changed from the base of the PR and between c26491b and 512124e.

⛔ Files ignored due to path filters (2)
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux.png is excluded by !**/*.png
📒 Files selected for processing (1)
  • app/api/builder/agent/route.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/api/builder/agent/route.ts

📝 Walkthrough

Walkthrough

Project Studio now passes request-scoped build intents, tracks Project V2 lifecycle states, and coordinates automatic build settlement. Tests verify canonical URLs and single execution. Sandbox cleanup requests are limited to 50 sorted records.

Changes

Studio build lifecycle

Layer / File(s) Summary
Build intent and navigation contract
components/drops-studio.tsx, lib/studio-build-intent.ts, tests/studio-build-intent.test.mjs
Studio navigation adds buildRequest. Helpers consume and clear matching one-shot parameters while preserving unrelated URL state.
Automatic build request settlement
app/api/builder/agent/route.ts, tests/builder-agent-route.test.mjs
The builder route claims request-scoped runs, returns 202 for active duplicates, and records terminal results.
Request-scoped automatic build execution
components/project-v2-studio-surface.tsx
Automatic builds track Sandbox readiness, retry in-progress requests, suppress duplicate events, and settle request-specific state.
Project lifecycle state and presentation
components/project-studio.tsx, app/styles/project-studio.inspector.css
Project Studio tracks idle, running, ready, and blocked states. The header and quality panel display lifecycle-specific status and guidance.
Studio flow validation
e2e/contracts/v0-studio-flow.spec.ts, e2e/fixtures/project-v2-ui-test.ts
The E2E flow verifies transient URL removal, canonical navigation, fixture initialization, and one builder-agent request after reload.

Sandbox cleanup limits

Layer / File(s) Summary
Sorted cleanup batch behavior
app/api/builder/cleanup/route.ts, lib/vercel-sandbox-runtime-adapter.ts, tests/vercel-sandbox-runtime-cleanup.test.mjs, tests/vercel-sandbox-runtime.test.mjs
Cleanup requests use a provider list limit of 50 and sortBy: "name". Tests verify pagination and idle sandbox processing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DropsStudio
  participant ProjectStudio
  participant ProjectV2StudioSurface
  participant BuilderAgentRoute
  participant ProjectRunState
  DropsStudio->>ProjectStudio: Open Studio with buildRequest
  ProjectStudio->>ProjectV2StudioSurface: Pass automatic build request
  ProjectV2StudioSurface->>BuilderAgentRoute: Submit buildRequestId
  BuilderAgentRoute->>ProjectRunState: Claim or settle automatic build
  BuilderAgentRoute-->>ProjectV2StudioSurface: Return build result or 202
  ProjectV2StudioSurface-->>ProjectStudio: Emit lifecycle and settlement events
  ProjectStudio->>ProjectStudio: Update lifecycle status and URL
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: preventing repeated Studio builds and fixing Sandbox cleanup.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/drops-studio-v0-runtime-resume

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c215f5f18c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +25 to +26
params.delete(STUDIO_AUTO_BUILD_PARAM);
params.delete(STUDIO_BUILD_REQUEST_PARAM);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep auto-build intent recoverable after disconnects

When a user reloads or opens Connections while the long-running initial build is active, app/api/builder/agent/route.ts aborts and stops the Sandbox as soon as the request disconnects. These parameters have already been deleted, and buildRequest is never sent to the builder API or persisted as a resumable job ID, so returning to Studio cannot resume or re-enqueue the interrupted creation build and instead requires a manual Retry. Preserve the intent until a terminal receipt exists, or implement server-side idempotent recovery keyed by this request ID.

AGENTS.md reference: AGENTS.md:L109-L109

Useful? React with 👍 / 👎.

Comment on lines +948 to 949
sortBy: "name",
limit,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Advance cleanup beyond the first name-sorted page

When an account has more than 50 ds2- Sandboxes, every cleanup invocation requests the same name-sorted first page and then stops after 50 inspected records. Because recent or already-stopped records still consume that inspection quota and no cursor or status filter advances the scan, later names can remain permanently uninspected and leak persistent Sandbox resources; paginate through subsequent records or persist/rotate a cursor between bounded runs.

Useful? React with 👍 / 👎.

Comment on lines 672 to 674
statusTimer = setInterval(() => {
void refreshSandboxStatus(snapshot.project.id).catch(() => undefined);
void refreshBuildProgress().catch(() => undefined);
}, 4_000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Throttle progress polling below the runtime-action quota

When a build runs near the 270-second server deadline, this four-second interval issues roughly 68 /api/builder/runtime status requests. That route shares the builder-runtime-action limit of 120 requests per hour, so two long build or repair attempts can exhaust the quota and make subsequent status, log, stop, and manual runtime actions return 429 for the rest of the window. Poll less frequently, reserve a separate bounded status quota, or stop polling after a safe request budget.

AGENTS.md reference: AGENTS.md:L109-L109

Useful? React with 👍 / 👎.

Comment on lines +4955 to +4956
? buildLifecycle === "running"
? "Building and checking your app"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render in-progress builds with a neutral status

Whenever buildLifecycle is running but the five release checks are not yet complete, releaseEvidenceReady remains false and the surrounding quality card keeps the failed class, whose stylesheet renders the card in red error colors. The newly added “Building and checking your app” message therefore appears inside a visual failure state during every normal build; add a running/pending class and reserve failed for the blocked lifecycle.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/styles/project-studio.inspector.css`:
- Line 129: Replace the new pending-state CSS rules in .quality-hero.pending and
.quality-pending with equivalent Tailwind CSS v4 utility classes or existing
project-local Tailwind tokens at the components using these variants. Preserve
the current pending background and text colors, and remove the raw
pending-specific CSS declarations from project-studio.inspector.css.

In `@components/project-v2-studio-surface.tsx`:
- Around line 1044-1049: Update the active build stop handling around
autoBuildRequestId to track the request ID that launched the active run. Remove
the sessionStorage lease and call onAutoBuildSettled only when that owner ID
matches the pending automatic request ID; leave the lease intact when stopping a
manual or otherwise unrelated build.
- Around line 64-65: Update the automatic-build flow using AUTO_BUILD_LEASE_MS
and the builder request around lines 802-819 so the request lease remains
durable until the build reaches a terminal state instead of expiring after eight
seconds. Include the existing autoBuildRequestId in the builder API payload,
allowing server-side idempotency to deduplicate retries after reloads or
returning to Studio. Preserve polling and release the lease only after
settlement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ea4671b-e84f-4662-b771-46c14b13f3db

📥 Commits

Reviewing files that changed from the base of the PR and between c215f5f and 1416f4f.

⛔ Files ignored due to path filters (2)
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux-system.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • app/styles/project-studio.inspector.css
  • components/project-studio.tsx
  • components/project-v2-studio-surface.tsx
  • lib/studio-build-intent.ts
  • lib/vercel-sandbox-runtime-adapter.ts
  • tests/studio-build-intent.test.mjs
  • tests/vercel-sandbox-runtime.test.mjs
💤 Files with no reviewable changes (1)
  • lib/vercel-sandbox-runtime-adapter.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/studio-build-intent.test.mjs
  • components/project-studio.tsx

.file-tree { border: 1px solid #dde6f1; border-radius: 10px; overflow: hidden; }
.file-tree span,.file-tree button { align-items: center; background: white; border: 0; border-bottom: 1px solid #edf1f6; color: #52667f; display: grid; font-size: 12px; gap: 7px; grid-template-columns: 15px 1fr auto; padding: 9px; text-align: left; width: 100%; }.file-tree span:last-child { border-bottom: 0; }.file-tree button:hover { background: #f3f7ff; color: #2f62c4; }.file-tree svg { color: #4774cf; height: 12px; width: 12px; }.file-tree b { color: #52617a; font-size: 12px; }
.quality-hero { align-items: center; border: 1px solid; border-radius: 11px; display: grid; gap: 9px; grid-template-columns: 38px 1fr; margin-bottom: 12px; padding: 11px; }.quality-hero > span { align-items: center; border-radius: 10px; display: flex; height: 36px; justify-content: center; }.quality-hero svg { height: 18px; width: 18px; }.quality-hero > div { display: grid; gap: 4px; }.quality-hero strong { font-size: 12px; }.quality-hero small { font-size: 12px; line-height: 1.45; }.quality-hero.passed { background: #eff9f5; border-color: #bde6d5; color: #217558; }.quality-hero.passed > span { background: #dff4eb; }.quality-hero.failed { background: #fff4f2; border-color: #f0c8c2; color: #a54b41; }.quality-hero.failed > span { background: #f9e5e1; }.quality-list { border: 1px solid #e0e7f1; border-radius: 10px; overflow: hidden; }.quality-list > div { align-items: center; border-bottom: 1px solid #edf1f6; display: grid; gap: 7px; grid-template-columns: 22px 1fr auto; padding: 8px; }.quality-list > div:last-child { border-bottom: 0; }.quality-list > div > span { align-items: center; background: #e8f7f0; border-radius: 50%; color: #16875b; display: flex; height: 20px; justify-content: center; width: 20px; }.quality-list > div.failed > span { background: #fde9e6; color: #c54c42; }.quality-list svg { height: 10px; width: 10px; }.quality-list > div > div { display: grid; gap: 2px; }.quality-list strong { color: #41546f; font-size: 12px; }.quality-list small { color: #8b98aa; font-size: 12px; line-height: 1.35; }.quality-list b { background: #edf3ff; border-radius: 99px; color: #3764b5; font-size: 12px; padding: 4px 5px; }.quality-pass { background: #e7f8ef!important; color: #16865b!important; }.quality-fail { background: #fff0ed!important; color: #bc4a42!important; }
.quality-hero { align-items: center; border: 1px solid; border-radius: 11px; display: grid; gap: 9px; grid-template-columns: 38px 1fr; margin-bottom: 12px; padding: 11px; }.quality-hero > span { align-items: center; border-radius: 10px; display: flex; height: 36px; justify-content: center; }.quality-hero svg { height: 18px; width: 18px; }.quality-hero > div { display: grid; gap: 4px; }.quality-hero strong { font-size: 12px; }.quality-hero small { font-size: 12px; line-height: 1.45; }.quality-hero.passed { background: #eff9f5; border-color: #bde6d5; color: #217558; }.quality-hero.passed > span { background: #dff4eb; }.quality-hero.pending { background: #f3f7ff; border-color: #c9d8f8; color: #3764a5; }.quality-hero.pending > span { background: #e4edff; }.quality-hero.failed { background: #fff4f2; border-color: #f0c8c2; color: #a54b41; }.quality-hero.failed > span { background: #f9e5e1; }.quality-list { border: 1px solid #e0e7f1; border-radius: 10px; overflow: hidden; }.quality-list > div { align-items: center; border-bottom: 1px solid #edf1f6; display: grid; gap: 7px; grid-template-columns: 22px 1fr auto; padding: 8px; }.quality-list > div:last-child { border-bottom: 0; }.quality-list > div > span { align-items: center; background: #e8f7f0; border-radius: 50%; color: #16875b; display: flex; height: 20px; justify-content: center; width: 20px; }.quality-list > div.failed > span { background: #fde9e6; color: #c54c42; }.quality-list svg { height: 10px; width: 10px; }.quality-list > div > div { display: grid; gap: 2px; }.quality-list strong { color: #41546f; font-size: 12px; }.quality-list small { color: #8b98aa; font-size: 12px; line-height: 1.35; }.quality-list b { background: #edf3ff; border-radius: 99px; color: #3764b5; font-size: 12px; padding: 4px 5px; }.quality-pass { background: #e7f8ef!important; color: #16865b!important; }.quality-pending { background: #edf3ff!important; color: #3764a5!important; }.quality-fail { background: #fff0ed!important; color: #bc4a42!important; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move the new pending-state styles to Tailwind utilities.

Line 129 adds raw .quality-hero.pending and .quality-pending rules. Use Tailwind CSS v4 classes or project-local Tailwind tokens for these new variants.

As per coding guidelines, “Use Tailwind CSS v4, Base UI 1.6 for new primitives, Lucide icons, and Motion only for short purposeful transitions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/styles/project-studio.inspector.css` at line 129, Replace the new
pending-state CSS rules in .quality-hero.pending and .quality-pending with
equivalent Tailwind CSS v4 utility classes or existing project-local Tailwind
tokens at the components using these variants. Preserve the current pending
background and text colors, and remove the raw pending-specific CSS declarations
from project-studio.inspector.css.

Source: Coding guidelines

Comment thread components/project-v2-studio-surface.tsx Outdated
Comment thread components/project-v2-studio-surface.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
components/project-v2-studio-surface.tsx (2)

662-688: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the sandbox view update against a late poll response.

refreshBuildProgress awaits refreshSandboxStatus at Line 663 before it checks runSettled and controller.signal.aborted. refreshSandboxStatus calls setSandbox at Line 546 and checks only mounted.current.

The interval is cleared at Lines 704-707, but a poll already in flight at that moment still resolves. It can resolve after absorbBuilderResult has written the final sandbox state at Line 611. The panel then shows a stale sandbox status until the next refresh.

Capture the result and apply it only when the run has not settled.

🛠️ Proposed fix
       const refreshBuildProgress = async () => {
+        if (runSettled || controller.signal.aborted) return;
         const state = await refreshSandboxStatus(snapshot.project.id);

This does not close the window completely. To close it, refreshSandboxStatus needs an optional guard so the caller can suppress the setSandbox write:

-  const refreshSandboxStatus = useCallback(async (projectId: string) => {
+  const refreshSandboxStatus = useCallback(async (
+    projectId: string,
+    shouldApply: () => boolean = () => true,
+  ) => {
     ...
-    if (mounted.current) setSandbox(sandboxView(state, project.preview?.error));
+    if (mounted.current && shouldApply()) {
+      setSandbox(sandboxView(state, project.preview?.error));
+    }
     return state;
   }, [project.preview?.error]);

Then call it as refreshSandboxStatus(snapshot.project.id, () => !runSettled && !controller.signal.aborted).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/project-v2-studio-surface.tsx` around lines 662 - 688, Update
refreshSandboxStatus to accept an optional write guard and check it before
applying its setSandbox update. In refreshBuildProgress, call
refreshSandboxStatus with a guard that returns true only while runSettled is
false and controller.signal.aborted is false, preventing late in-flight polls
from overwriting the final sandbox state.

699-719: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

A 202 response drives the UI into a false blocked state.

onTerminalResponse runs at Line 702 and sets retryAfterInProgress for a 202, but runBuilder then continues. The 202 body from app/api/builder/agent/route.ts Lines 368-375 is { code, status, project } and contains no result. So Line 709 throws "Builder agent returned no verifiable result.".

That throw reaches the catch at Line 756. The catch then:

  • sets agentState to blocked at Line 763,
  • sets release.status to blocked with the error as a blocker at Line 767,
  • sets the sandbox status to failed at Line 770,
  • emits a blocked agent event at Lines 774-778,
  • calls onNotify with the error text at Line 779.

A 202 means the same build is already running on the server. That is the normal deduplication path this PR adds. The user sees a build failure notification and a blocked panel on every retry cycle, roughly every 10 seconds, until the original build settles.

Return early on 202 before the payload check.

🛠️ Proposed fix
       options.onTerminalResponse?.(response);
       runSettled = true;
       if (statusTimer) {
         clearInterval(statusTimer);
         statusTimer = null;
       }
+      if (response.status === 202) {
+        onAgentEvent?.({
+          phase: activePhase,
+          status: "active",
+          message: "The automatic build is already running. Waiting for it to finish…",
+        });
+        return null;
+      }
       const payload = await responseJson<BuilderApiPayload>(response);
       if (!payload.result) {
         throw new Error(payload.error ?? "Builder agent returned no verifiable result.");
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/project-v2-studio-surface.tsx` around lines 699 - 719, Update
runBuilder to return immediately after onTerminalResponse when the response
status is 202, before calling responseJson or checking payload.result. Preserve
the existing terminal cleanup and deduplication behavior so an already-running
build does not enter the catch block or mark the agent, release, or sandbox as
blocked.
app/api/builder/agent/route.ts (1)

378-390: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The auto:<requestId> run can be left permanently in running, and the client cannot escape that state. The claim writes running to durable storage, but three gaps prevent a guaranteed transition to a terminal status. The client then polls forever because it treats a persisted running run as proof that a build is still in progress.

  • app/api/builder/agent/route.ts#L378-L390: move the start of the settlement try to immediately after the claim, so a throw from new ServerBuilderAuditSink() or new VercelSandboxRuntimeAdapter({ audit }) still settles the run.
  • app/api/builder/agent/route.ts#L146-L164: add bounded revision-conflict retries inside settleAutoBuildRequest, and make a settle failure on the success path non-fatal so a verified build is not reported as an error.
  • components/project-v2-studio-surface.tsx#L819-L833: cap the retry attempts on the persisted running or queued branch. When the cap is reached, stop reloading the project, report the state through onNotify, and call onAutoBuildSettled.

As per coding guidelines, "Every external or destructive tool must have explicit approval, timeout, quota, audit record, bounded output, and idempotency behavior."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/builder/agent/route.ts` around lines 378 - 390, Ensure auto-build
claims always reach a terminal state: in app/api/builder/agent/route.ts lines
378-390, begin the settlement try immediately after the claim so
ServerBuilderAuditSink or VercelSandboxRuntimeAdapter construction failures are
settled; in app/api/builder/agent/route.ts lines 146-164, add bounded
revision-conflict retries to settleAutoBuildRequest and keep success-path
settlement failures non-fatal; in components/project-v2-studio-surface.tsx lines
819-833, cap retries for persisted running or queued states, then stop project
reloads, notify via onNotify, and call onAutoBuildSettled.

Source: Coding guidelines

🧹 Nitpick comments (3)
components/project-v2-studio-surface.tsx (1)

66-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the automatic run-id format instead of duplicating it.

autoBuildRunId here is identical to autoBuildRunId in app/api/builder/agent/route.ts at Lines 69-71. The auto:${requestId} string is now a contract between the server, which writes the run, and this component, which reads the run at Line 816 to decide whether to launch a build.

If one side changes the prefix, this component stops recognizing persisted runs. It then launches a duplicate build after every reload. That is the exact failure this PR fixes.

Export a single helper from a shared module and import it in both files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/project-v2-studio-surface.tsx` around lines 66 - 68, Move the
shared automatic run-ID helper and its `auto:${requestId}` format into a shared
module, export it there, and import and use it from both
`project-v2-studio-surface.tsx` and `app/api/builder/agent/route.ts`. Remove the
duplicate local `autoBuildRunId` definitions while preserving the existing
run-ID behavior.
tests/builder-agent-route.test.mjs (2)

405-408: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This assertion does not prove that settleAutoBuildRequest set the stopped status.

On the cancellation path the route runs stopInterruptedSession first. That function maps every run with status running to stopped and saves the project. settleAutoBuildRequest then loads the project, finds run.status !== "running", and returns early without writing.

So the stopped value asserted here comes from stopInterruptedSession, not from the new settle path. The test passes even if the stopped branch of settleAutoBuildRequest is removed.

Two gaps remain untested in this file:

  1. The stopped branch of settleAutoBuildRequest.
  2. The revision-conflict retry in claimAutoBuildRequest. The mock saveAuthorized at Line 60 never changes stored.revision, because withAutoBuildRun does not increment revision. The conflict path never executes.

Add a case where saveAuthorized rejects once with a revision conflict, then verify that the claim recovers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/builder-agent-route.test.mjs` around lines 405 - 408, Strengthen the
tests in builder-agent-route.test.mjs so the stopped-status assertion
specifically exercises settleAutoBuildRequest after stopInterruptedSession,
rather than relying on the earlier mutation; add a separate
claimAutoBuildRequest case where saveAuthorized rejects once with a revision
conflict and then succeeds, updating the mock revision as needed, and verify the
claim recovers.

390-395: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the claim was observed before continuing.

The loop waits up to 100 ms for the auto:<buildRequestId> run to reach running. If the claim does not land in that window, the loop exits silently. The next assertion at Line 400 then fails with a status mismatch, which hides the real cause.

Add an explicit assertion after the loop.

♻️ Proposed change
   for (let attempt = 0; attempt < 20; attempt += 1) {
     if (deps.getStored().runs.some(
       (run) => run.id === `auto:${buildRequestId}` && run.status === "running",
     )) break;
     await new Promise((resolve) => setTimeout(resolve, 5));
   }
+  assert.equal(
+    deps.getStored().runs.find((run) => run.id === `auto:${buildRequestId}`)?.status,
+    "running",
+    "the automatic build request was not claimed before the duplicate request",
+  );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/builder-agent-route.test.mjs` around lines 390 - 395, In the test’s
polling block around the attempt loop, track whether the
`auto:${buildRequestId}` run reached `running` and add an explicit assertion
immediately after the loop that the claim was observed. Keep the existing wait
behavior, but fail with a clear assertion when the 100 ms window expires before
the subsequent status checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/builder/agent/route.ts`:
- Around line 96-103: Update the run-retention logic before the returned project
object so active runs with status “running” are retained even when they fall
outside the latest 256 runs. Build the retained run set by preserving those
active runs alongside the newest 256 entries, then continue filtering logs
through retainedRunIds so persisted logs for active runs are not removed.
- Line 394: Update the audit event correlation around the requestId field to
include the actorHash alongside the requestId, and require consumers to match
both values when locating auto-build records. Preserve the existing generated
requestId fallback while preventing client-controlled IDs from correlating
across actors.

---

Outside diff comments:
In `@app/api/builder/agent/route.ts`:
- Around line 378-390: Ensure auto-build claims always reach a terminal state:
in app/api/builder/agent/route.ts lines 378-390, begin the settlement try
immediately after the claim so ServerBuilderAuditSink or
VercelSandboxRuntimeAdapter construction failures are settled; in
app/api/builder/agent/route.ts lines 146-164, add bounded revision-conflict
retries to settleAutoBuildRequest and keep success-path settlement failures
non-fatal; in components/project-v2-studio-surface.tsx lines 819-833, cap
retries for persisted running or queued states, then stop project reloads,
notify via onNotify, and call onAutoBuildSettled.

In `@components/project-v2-studio-surface.tsx`:
- Around line 662-688: Update refreshSandboxStatus to accept an optional write
guard and check it before applying its setSandbox update. In
refreshBuildProgress, call refreshSandboxStatus with a guard that returns true
only while runSettled is false and controller.signal.aborted is false,
preventing late in-flight polls from overwriting the final sandbox state.
- Around line 699-719: Update runBuilder to return immediately after
onTerminalResponse when the response status is 202, before calling responseJson
or checking payload.result. Preserve the existing terminal cleanup and
deduplication behavior so an already-running build does not enter the catch
block or mark the agent, release, or sandbox as blocked.

---

Nitpick comments:
In `@components/project-v2-studio-surface.tsx`:
- Around line 66-68: Move the shared automatic run-ID helper and its
`auto:${requestId}` format into a shared module, export it there, and import and
use it from both `project-v2-studio-surface.tsx` and
`app/api/builder/agent/route.ts`. Remove the duplicate local `autoBuildRunId`
definitions while preserving the existing run-ID behavior.

In `@tests/builder-agent-route.test.mjs`:
- Around line 405-408: Strengthen the tests in builder-agent-route.test.mjs so
the stopped-status assertion specifically exercises settleAutoBuildRequest after
stopInterruptedSession, rather than relying on the earlier mutation; add a
separate claimAutoBuildRequest case where saveAuthorized rejects once with a
revision conflict and then succeeds, updating the mock revision as needed, and
verify the claim recovers.
- Around line 390-395: In the test’s polling block around the attempt loop,
track whether the `auto:${buildRequestId}` run reached `running` and add an
explicit assertion immediately after the loop that the claim was observed. Keep
the existing wait behavior, but fail with a clear assertion when the 100 ms
window expires before the subsequent status checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4dddb87c-3533-40ec-9dfc-cdcdde9e4c34

📥 Commits

Reviewing files that changed from the base of the PR and between 1416f4f and c26491b.

📒 Files selected for processing (3)
  • app/api/builder/agent/route.ts
  • components/project-v2-studio-surface.tsx
  • tests/builder-agent-route.test.mjs

Comment on lines +96 to +103
].slice(-256);
const retainedRunIds = new Set(runs.map((run) => run.id));
return {
...project,
runs,
logs: project.logs.filter((log) => retainedRunIds.has(log.runId)),
updatedAt: now,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the log metadata type and every writer of project logs / runIds.
set -euo pipefail

rg -nP -C4 'ProjectV2LogMetadataV2' --type=ts | head -60

echo '--- runId assignments ---'
rg -nP -C3 '\brunId\s*:' --type=ts --type=tsx -g '!**/node_modules/**' | head -120

echo '--- writers into project.logs ---'
rg -nP -C5 '\blogs\s*:\s*\[' --type=ts -g '!**/node_modules/**' | head -120

Repository: svg8bit/drops-studio

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -u

echo '--- candidate files ---'
git ls-files | rg '(^|/)(route\.ts|.*project.*|.*builder.*|.*studio.*)\.(ts|tsx|js|jsx)$' | head -200

echo '--- target implementation ---'
sed -n '1,150p' app/api/builder/agent/route.ts 2>/dev/null || true

echo '--- log type and project log references ---'
rg -n -C3 'ProjectV2LogMetadataV2|project\.logs|logs:\s*project\.logs|runId' --glob '*.{ts,tsx,js,jsx}' . | head -300

Repository: svg8bit/drops-studio

Length of output: 28010


🏁 Script executed:

#!/bin/bash
set -u

echo '--- route claim/settle call sites ---'
rg -n -C8 'withAutoBuildRun|claimAutoBuildRequest|settleAutoBuildRequest|runIntelligentBuilderAgent|runBuilderAgent' app/api/builder/agent/route.ts

echo '--- route remainder ---'
sed -n '145,360p' app/api/builder/agent/route.ts

echo '--- workspace run/log materialization ---'
sed -n '70,165p' lib/builder-agent/workspace.ts
sed -n '780,835p' lib/builder-agent/workspace.ts

echo '--- project run/log schema and persistence validation ---'
sed -n '100,140p' lib/project-v2-types.ts
sed -n '155,180p' lib/project-v2-validator.ts
sed -n '360,400p' lib/project-v2-validator.ts

echo '--- runtime lifecycle references ---'
rg -n -C5 'toProjectExecution|materializ|runId|executeCommand|runCommand|saveAuthorized' lib/builder-agent lib/project-runtime-adapter.ts lib/vercel-sandbox-runtime-adapter.ts app/api/builder --glob '*.{ts,tsx}' | head -400

Repository: svg8bit/drops-studio

Length of output: 48773


🏁 Script executed:

#!/bin/bash
set -u

echo '--- preview run creation and live statuses ---'
rg -n -C12 'startPreview|`#preview`|runId:|exitCode: null|appendCommandMetadata' lib/builder-agent/workspace.ts lib/vercel-sandbox-runtime-adapter.ts

echo '--- retention-related tests and fixtures ---'
rg -n -C5 '256|2048|withAutoBuildRun|appendCommandMetadata|status:\s*"running"|Project log .*unknown run' --glob '*.{ts,tsx,js,jsx}' app lib e2e tests 2>/dev/null | head -300

echo '--- pure retention behavior probe ---'
python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class Run:
    id: str
    status: str

`@dataclass`
class Log:
    id: str
    runId: str

def with_auto_build_run(runs, logs, request_id, status):
    run_id = f"auto:{request_id}"
    prior = next((r for r in runs if r.id == run_id), None)
    next_runs = [r for r in runs if r.id != run_id]
    next_runs.append(Run(run_id, status))
    next_runs = next_runs[-256:]
    retained = {r.id for r in next_runs}
    return next_runs, [log for log in logs if log.runId in retained]

runs = [Run(f"run-{i}", "running" if i == 0 else "succeeded") for i in range(256)]
logs = [Log("log-live", "run-0")]
next_runs, next_logs = with_auto_build_run(runs, logs, "request-1", "running")
print("live run retained:", any(r.id == "run-0" for r in next_runs))
print("live log retained:", any(log.id == "log-live" for log in next_logs))
print("new auto run retained:", any(r.id == "auto:request-1" for r in next_runs))
PY

Repository: svg8bit/drops-studio

Length of output: 50376


🏁 Script executed:

python3 - <<'PY'
runs = [
    {"id": f"run-{i}", "status": "running" if i == 0 else "succeeded"}
    for i in range(256)
]
logs = [{"id": "log-live", "runId": "run-0"}]

next_runs = [run for run in runs if run["id"] != "auto:req-1"]
next_runs.append({"id": "auto:req-1", "status": "running"})
next_runs = next_runs[-256:]
retained = {run["id"] for run in next_runs}
next_logs = [log for log in logs if log["runId"] in retained]

print({
    "live_run_retained": "run-0" in retained,
    "live_log_retained": any(log["id"] == "log-live" for log in next_logs),
    "auto_run_retained": "auto:req-1" in retained,
})
PY

Repository: svg8bit/drops-studio

Length of output: 242


Preserve logs for active runs during retention. .slice(-256) can evict a running run, and the following filter then deletes its persisted logs during automatic-build claim or settlement. Retain active runs before pruning. The validator already rejects logs whose runId does not reference a project run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/builder/agent/route.ts` around lines 96 - 103, Update the
run-retention logic before the returned project object so active runs with
status “running” are retained even when they fall outside the latest 256 runs.
Build the retained run set by preserving those active runs alongside the newest
256 entries, then continue filtering logs through retainedRunIds so persisted
logs for active runs are not removed.

const session = new BuilderAgentSession({
actorId,
requestId: randomUUID(),
requestId: parsed.data.buildRequestId ?? randomUUID(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how BuilderAgentSession stores and consumes requestId.
set -euo pipefail

fd -t f 'session.ts' lib/builder-agent

echo '--- requestId usages in builder-agent ---'
rg -nP -C5 '\brequestId\b' lib/builder-agent --type=ts | head -150

echo '--- audit record shapes ---'
rg -nP -C5 'record\s*\(' lib/builder-agent --type=ts | head -80

Repository: svg8bit/drops-studio

Length of output: 13683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- session construction and requestId propagation ---'
rg -n -C8 'new BuilderAgentSession|buildRequestId|requestId:|runBuilderAgent|BuilderAgentSession' app lib --glob '*.{ts,tsx,js,jsx}' | head -300

echo '--- all audit sink implementations and requestId consumers ---'
rg -n -C8 'BuilderAgentAuditSink|record\(event|actorHash|requestId|rate.?limit|audit' app lib --glob '*.{ts,tsx,js,jsx}' | head -500

echo '--- candidate files ---'
fd -t f . app lib | rg '(builder|audit|session|rate|runtime|workspace)' | head -200

Repository: svg8bit/drops-studio

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- exact builder-agent audit sink references ---'
rg -n 'BuilderAgentAuditSink|MemoryBuilderAgentAuditSink|audit:\s*|audit\.record|new BuilderAgentSession|buildRequestId' app lib tests __tests__ 2>/dev/null || true

echo '--- route request construction ---'
sed -n '330,430p' app/api/builder/agent/route.ts

echo '--- complete builder-agent source file list ---'
fd -t f . lib/builder-agent app/api/builder/agent

echo '--- requestId field consumers outside declarations and event construction ---'
rg -n 'requestId' app lib --glob '*.{ts,tsx,js,jsx}' \
  | rg -v 'interface |type |requestId: context\.requestId|requestId: input\.requestId|requestId: dependencies\.services\.requestId|readonly requestId|requestId: string|requestId\?:|providerRequestId|STUDIO_BUILD_REQUEST_PARAM' \
  | head -250

Repository: svg8bit/drops-studio

Length of output: 22889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- auto-build request claim and run storage ---'
sed -n '45,175p' app/api/builder/agent/route.ts

echo '--- server builder audit sink ---'
sed -n '1,150p' app/api/builder/shared.ts

echo '--- build route completion and response paths ---'
sed -n '470,555p' app/api/builder/agent/route.ts

echo '--- runtime route session construction ---'
sed -n '180,225p' app/api/builder/runtime/route.ts

echo '--- runtime and builder audit sink types/implementations ---'
rg -n -C12 'interface RuntimeAuditSink|class .*AuditSink|ServerBuilderAuditSink|record\(event' lib app/api/builder --glob '*.{ts,tsx}'

Repository: svg8bit/drops-studio

Length of output: 28610


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

files = {
    "route": Path("app/api/builder/agent/route.ts").read_text(),
    "shared": Path("app/api/builder/shared.ts").read_text(),
    "policy": Path("lib/builder-agent/policy.ts").read_text(),
    "runtime": Path("lib/project-runtime-adapter.ts").read_text(),
}

checks = {
    "auto-build storage calls are actor-authorized": (
        "saveAuthorized(\n        input.actorId," in files["route"]
        and "loadAuthorized(\n        input.actorId," in files["route"]
    ),
    "builder audit event retains requestId and actorHash separately": (
        "requestId: input.requestId" in files["policy"]
        and "actorHash: runtimeActorHash(input.actorId)" in files["policy"]
    ),
    "runtime audit event retains requestId and actorHash separately": (
        "requestId: context.requestId" in files["runtime"]
        and "actorHash: runtimeActorHash(context.actorId)" in files["runtime"]
    ),
    "server sink writes event without a composite correlation key": (
        'JSON.stringify({ source: "drops-studio-builder", ...event })' in files["shared"]
    ),
    "memory audit sink does not key records by requestId": (
        "this.events.push(structuredClone(event))" in files["policy"]
    ),
}

for name, result in checks.items():
    print(f"{name}: {'yes' if result else 'no'}")
PY

Repository: svg8bit/drops-studio

Length of output: 468


Scope audit correlation by actor. Auto-build storage is actor-scoped, but audit events emit the client-controlled requestId as a standalone field. Require audit consumers to match both actorHash and requestId to prevent cross-actor correlation collisions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/builder/agent/route.ts` at line 394, Update the audit event
correlation around the requestId field to include the actorHash alongside the
requestId, and require consumers to match both values when locating auto-build
records. Preserve the existing generated requestId fallback while preventing
client-controlled IDs from correlating across actors.

@svg8bit
svg8bit merged commit e4e34f1 into main Aug 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant