Skip to content

Commit ef86735

Browse files
authored
fix(agent): preserve uploads during app bootstrap (#80)
## Summary - preserves `/uploads` when the first template workspace is scaffolded - recognizes both `src/app` and `app` Expo Router layouts so valid projects are restored - reuses an existing pnpm installation and only installs when `node_modules/.pnpm` is missing ## Context Direct production QA showed a successful upload and visible composer chip, followed by an app-builder run that deleted the uploaded file and performed a redundant dependency install. The install then failed before the model could read the file. ## Decisions | Decision | Choice | Reasoning | |---|---|---| | Upload persistence | Exclude `uploads` from first-run template cleanup | The project volume and R2 file record remain authoritative across chats | | Workspace detection | Accept `src/app` and `app` | Both supported template layouts are valid and must not trigger destructive bootstrap | | Dependency restore | Install only when the pnpm virtual store is absent | Persistent project workspaces should not reinstall on every prompt | ## Edge cases - first prompt uploads survive template scaffolding - existing Expo projects using either Router layout avoid destructive reset - missing dependency stores still take the existing install/recovery path - imported repository reset behavior is unchanged ## Verification - [x] `pnpm turbo typecheck lint build` — 76/76 tasks passed - [x] production migration verifier already passes against the single migration file - [ ] repeat direct production upload, later-reference, and agent-read QA after deploy No Linear issue or plan document is associated with this production QA fix.
1 parent 50cd06c commit ef86735

2 files changed

Lines changed: 59 additions & 13 deletions

File tree

apps/agent-worker/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ the project path and content digest. R2 stores immutable bytes under the existin
4040
small current/version namespace records and mirrors the current version to
4141
`/workspace/<workspaceSlug>/uploads/` on the user's persistent Daytona volume before exposing it.
4242
An exact replay is idempotent; uploading new bytes at the same path creates a retained version and
43-
updates the working copy. Project deletion removes the namespace during fenced workspace cleanup
44-
and the existing resource-deletion prefix sweep removes every immutable object. Account deletion
45-
clears both through the existing account state and R2 lifecycle phases.
43+
updates the working copy. First-run app scaffolding preserves the `uploads/` directory, and restored
44+
template projects reuse their persistent dependency installation instead of rebuilding the
45+
workspace. Project deletion removes the namespace during fenced workspace cleanup and the existing
46+
resource-deletion prefix sweep removes every immutable object. Account deletion clears both through
47+
the existing account state and R2 lifecycle phases.
4648

4749
Run creation validates the gateway payload with the shared `CreateRunSchema` from
4850
`packages/types` before selecting the run-scoped `AgentRun` Durable Object. The

apps/agent-worker/src/durable-objects/agent-run-app-builder.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,7 @@ export async function runAppBuilder(
160160
if (await hasImportedAppWorkspace(sandbox, workspace.dir)) {
161161
return restoreImportedWorkspace(workspaceOptions);
162162
}
163-
const shouldBootstrap = !(await hasExistingAppBuilderWorkspace(
164-
sandbox,
165-
workspace.dir,
166-
workspace.mobile,
167-
));
163+
const shouldBootstrap = !(await hasExistingAppBuilderWorkspace(sandbox, workspace.dir));
168164
if (shouldBootstrap && input.importRepoUrl) {
169165
return importRepoWorkspace({ ...workspaceOptions, repoUrl: input.importRepoUrl });
170166
}
@@ -200,13 +196,15 @@ async function prepareTemplateWorkspace(
200196
setRunStage(mobile ? "Preparing the Expo workspace." : "Preparing the Next.js workspace.");
201197
if (!shouldBootstrap) {
202198
setRunStage("Restoring the app workspace.");
203-
await installAppBuilderDependencies(sandbox, logger, workspace.dir, mobile);
199+
if (!(await hasInstalledAppBuilderDependencies(sandbox, workspace.dir))) {
200+
await installAppBuilderDependencies(sandbox, logger, workspace.dir, mobile);
201+
}
204202
if (mobile) {
205203
await ensureExpoWebSupport(sandbox, workspace.dir);
206204
}
207205
return;
208206
}
209-
await resetAppBuilderDirectory(sandbox, workspace.dir);
207+
await resetTemplateAppBuilderDirectory(sandbox, workspace.dir);
210208
throwIfRunCanceled(options.abortSignal);
211209
if (mobile) {
212210
await scaffoldExpoApp(sandbox, logger, workspace.dir);
@@ -583,12 +581,28 @@ async function startAppBuilderDevServer(
583581
async function hasExistingAppBuilderWorkspace(
584582
sandbox: ProjectSandboxStub,
585583
dir: string,
586-
mobile: boolean,
587584
): Promise<boolean> {
588-
const appDir = mobile ? "app" : "src/app";
585+
const appDirs = ["src/app", "app"];
586+
const result = await executeShellTerminal(
587+
{
588+
command:
589+
`test -f ${dir}/package.json && ` +
590+
`(test -d ${dir}/${appDirs[0]} || test -d ${dir}/${appDirs[1]})`,
591+
cwd: "/workspace",
592+
timeoutMs: 10_000,
593+
},
594+
{ sandbox },
595+
);
596+
return result.success;
597+
}
598+
599+
async function hasInstalledAppBuilderDependencies(
600+
sandbox: ProjectSandboxStub,
601+
dir: string,
602+
): Promise<boolean> {
589603
const result = await executeShellTerminal(
590604
{
591-
command: `test -f ${dir}/package.json && test -d ${dir}/${appDir}`,
605+
command: `test -d ${dir}/node_modules/.pnpm`,
592606
cwd: "/workspace",
593607
timeoutMs: 10_000,
594608
},
@@ -604,6 +618,36 @@ async function resetAppBuilderDirectory(sandbox: ProjectSandboxStub, dir: string
604618
);
605619
}
606620

621+
async function resetTemplateAppBuilderDirectory(
622+
sandbox: ProjectSandboxStub,
623+
dir: string,
624+
): Promise<void> {
625+
await executeShellExec(
626+
{
627+
command: [
628+
"find",
629+
dir,
630+
"-mindepth",
631+
"1",
632+
"-maxdepth",
633+
"1",
634+
"!",
635+
"-name",
636+
"uploads",
637+
"-exec",
638+
"rm",
639+
"-rf",
640+
"--",
641+
"{}",
642+
"+",
643+
],
644+
cwd: "/workspace",
645+
timeoutMs: 120_000,
646+
},
647+
{ sandbox },
648+
);
649+
}
650+
607651
async function clearBuildCache(
608652
sandbox: ProjectSandboxStub,
609653
dir: string,

0 commit comments

Comments
 (0)