Fix repeated Studio builds and Sandbox cleanup - #29
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughProject 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. ChangesStudio build lifecycle
Sandbox cleanup limits
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 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".
| params.delete(STUDIO_AUTO_BUILD_PARAM); | ||
| params.delete(STUDIO_BUILD_REQUEST_PARAM); |
There was a problem hiding this comment.
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 👍 / 👎.
| sortBy: "name", | ||
| limit, |
There was a problem hiding this comment.
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 👍 / 👎.
| statusTimer = setInterval(() => { | ||
| void refreshSandboxStatus(snapshot.project.id).catch(() => undefined); | ||
| void refreshBuildProgress().catch(() => undefined); | ||
| }, 4_000); |
There was a problem hiding this comment.
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 👍 / 👎.
| ? buildLifecycle === "running" | ||
| ? "Building and checking your app" |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (2)
e2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1024-linux-system.pngis excluded by!**/*.pnge2e/visual/home.spec.ts-snapshots/studio-crypto-game-chromium-1440-linux-system.pngis excluded by!**/*.png
📒 Files selected for processing (7)
app/styles/project-studio.inspector.csscomponents/project-studio.tsxcomponents/project-v2-studio-surface.tsxlib/studio-build-intent.tslib/vercel-sandbox-runtime-adapter.tstests/studio-build-intent.test.mjstests/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; } |
There was a problem hiding this comment.
📐 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
There was a problem hiding this comment.
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 winGuard the sandbox view update against a late poll response.
refreshBuildProgressawaitsrefreshSandboxStatusat Line 663 before it checksrunSettledandcontroller.signal.aborted.refreshSandboxStatuscallssetSandboxat Line 546 and checks onlymounted.current.The interval is cleared at Lines 704-707, but a poll already in flight at that moment still resolves. It can resolve after
absorbBuilderResulthas 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,
refreshSandboxStatusneeds an optional guard so the caller can suppress thesetSandboxwrite:- 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 winA
202response drives the UI into a false blocked state.
onTerminalResponseruns at Line 702 and setsretryAfterInProgressfor a202, butrunBuilderthen continues. The202body fromapp/api/builder/agent/route.tsLines 368-375 is{ code, status, project }and contains noresult. So Line 709 throws"Builder agent returned no verifiable result.".That throw reaches the catch at Line 756. The catch then:
- sets
agentStatetoblockedat Line 763,- sets
release.statustoblockedwith the error as a blocker at Line 767,- sets the sandbox status to
failedat Line 770,- emits a
blockedagent event at Lines 774-778,- calls
onNotifywith the error text at Line 779.A
202means 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
202before 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 liftThe
auto:<requestId>run can be left permanently inrunning, and the client cannot escape that state. The claim writesrunningto durable storage, but three gaps prevent a guaranteed transition to a terminal status. The client then polls forever because it treats a persistedrunningrun as proof that a build is still in progress.
app/api/builder/agent/route.ts#L378-L390: move the start of the settlementtryto immediately after the claim, so a throw fromnew ServerBuilderAuditSink()ornew VercelSandboxRuntimeAdapter({ audit })still settles the run.app/api/builder/agent/route.ts#L146-L164: add bounded revision-conflict retries insidesettleAutoBuildRequest, 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 persistedrunningorqueuedbranch. When the cap is reached, stop reloading the project, report the state throughonNotify, and callonAutoBuildSettled.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 winShare the automatic run-id format instead of duplicating it.
autoBuildRunIdhere is identical toautoBuildRunIdinapp/api/builder/agent/route.tsat Lines 69-71. Theauto:${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 winThis assertion does not prove that
settleAutoBuildRequestset thestoppedstatus.On the cancellation path the route runs
stopInterruptedSessionfirst. That function maps every run with statusrunningtostoppedand saves the project.settleAutoBuildRequestthen loads the project, findsrun.status !== "running", and returns early without writing.So the
stoppedvalue asserted here comes fromstopInterruptedSession, not from the new settle path. The test passes even if thestoppedbranch ofsettleAutoBuildRequestis removed.Two gaps remain untested in this file:
- The
stoppedbranch ofsettleAutoBuildRequest.- The revision-conflict retry in
claimAutoBuildRequest. The mocksaveAuthorizedat Line 60 never changesstored.revision, becausewithAutoBuildRundoes not incrementrevision. The conflict path never executes.Add a case where
saveAuthorizedrejects 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 winAssert that the claim was observed before continuing.
The loop waits up to 100 ms for the
auto:<buildRequestId>run to reachrunning. 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
📒 Files selected for processing (3)
app/api/builder/agent/route.tscomponents/project-v2-studio-surface.tsxtests/builder-agent-route.test.mjs
| ].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, | ||
| }; |
There was a problem hiding this comment.
🗄️ 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 -120Repository: 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 -300Repository: 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 -400Repository: 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))
PYRepository: 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,
})
PYRepository: 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(), |
There was a problem hiding this comment.
🔒 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 -80Repository: 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 -200Repository: 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 -250Repository: 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'}")
PYRepository: 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.
Summary
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
Summary by CodeRabbit
New Features
Bug Fixes