Skip to content

Commit dda6210

Browse files
authored
fix: restore preview dependencies after sandbox wake (#202)
## Summary - Restore disposable pnpm dependencies before a persisted preview or pnpm-backed process relaunches in a replacement Daytona container. - Keep the immutable app-builder template on its zero-install fast path when both manifests match. - Tighten the FastApply tool contract to consistently produce valid sparse edits. ## Architecture Durable Object storage remains the source of truth for process launch intent and port reservations. On wake or runtime replacement, the Worker mirrors durable project source onto native sandbox disk, reconciles the disposable dependency tree, starts the source-sync loop, and then relaunches the persisted process. ## Decisions Made | Decision | Choice | Alternatives considered | Reasoning | |---|---|---|---| | Dependency recovery | Reconcile with pnpm before every custom process launch | Trust a `node_modules` presence marker | Presence can be stale after manifest changes; pnpm is the correctness boundary. | | Cold-start behavior | Offline-first frozen install with prefer-offline network fallback | Network-only install | Cached packages restore quickly while custom dependencies remain recoverable. | | Template behavior | Compare template package and lock manifests, then skip install | Always install | The immutable template already has a baked shared runtime and should remain instant. | | Edit guidance | Match Morph's exact marker-based contract | Allow ambiguous sparse edits | Explicit markers prevent accidental deletion and reduce invalid model calls. | ## Edge Cases Handled | Scenario | Handling | |---|---| | Custom dependencies disappear with a replacement container | Reinstall from the durable lockfile before process start. | | Cached package is unavailable offline | Retry through the registry with bounded network concurrency. | | Project has no lockfile | Perform a supported prefer-offline install. | | Unchanged scaffold uses only baked dependencies | Skip installation after exact manifest comparison. | | Sparse edit omits unchanged regions | Schema and tool guidance require the exact existing-code marker. | ## How to Review 1. Start with `project-sandbox-local-source.ts` for process ordering and dependency recovery. 2. Check the app-builder and generic process call sites. 3. Review the FastApply descriptions and README update. ## Verification - [x] `pnpm lint` - [x] `pnpm typecheck` - [x] `pnpm turbo build --force` - [x] `pnpm deadcode` - [x] `pnpm architecture:check` - [x] `pnpm turbo skills:build` - [x] Generated command assertions for ordering, offline fallback, lockless recovery, and template fast path - [x] Production explicit FastApply flow reached an atomic compare-and-swap write - [ ] Production cold-restore preview QA after Worker deployment and immutable snapshot promotion
1 parent 2f97a1c commit dda6210

6 files changed

Lines changed: 49 additions & 9 deletions

File tree

apps/agent-worker/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ content identities rather than unrelated FUSE/native-disk timestamps, performs a
158158
replacement, and mirrors subsequent writes and deletions within 250 ms, including equal-size and
159159
shell-based edits. The local source, dependency tree, and build cache are disposable;
160160
wake and restart reconstruct them from the durable project without changing the Files surface.
161+
Persisted pnpm-backed preview commands restore a missing sandbox-local dependency tree before the
162+
server starts, while an unchanged baked app-builder template continues to use the immutable runtime
163+
without an unnecessary install.
161164
The immutable sandbox exposes that synchronizer as the root-owned
162165
`/opt/cheatcode/project-source-sync.py` helper, keeping Worker-to-sandbox command arguments small
163166
and making the snapshot the source of truth for executable sandbox runtime code.

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { shellQuote } from "../sandbox-support";
2-
import { localProjectProcessCommand } from "./project-sandbox-local-source";
3-
import { NEXT_RUNTIME_BIN, resolveProjectLocalRuntime } from "./project-sandbox-package-runtime";
2+
import { localPnpmProjectProcessCommand } from "./project-sandbox-local-source";
3+
import {
4+
NEXT_RUNTIME_BIN,
5+
NEXT_TEMPLATE_DIR,
6+
resolveProjectLocalRuntime,
7+
} from "./project-sandbox-package-runtime";
48

59
interface LocalPreviewCommandInput {
610
port: number;
@@ -25,5 +29,5 @@ export function localNextPreviewCommand(input: LocalPreviewCommandInput): string
2529
]
2630
.map(shellQuote)
2731
.join(" ");
28-
return ["sh", "-lc", localProjectProcessCommand(runtime, nextCommand)];
32+
return ["sh", "-lc", localPnpmProjectProcessCommand(runtime, nextCommand, NEXT_TEMPLATE_DIR)];
2933
}

apps/agent-worker/src/durable-objects/project-sandbox-local-source.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,19 @@ export function localPackageCommand(
4242
}
4343

4444
/** Runs a long-lived package script on native disk while durable source changes keep flowing in. */
45-
export function localProjectProcessCommand(
45+
function localProjectProcessCommand(
4646
runtime: ProjectLocalRuntime,
4747
rawCommand: string,
48+
prepareCommand?: string,
4849
): string {
4950
const syncOnce = localSourceSyncCommand(runtime, "preview-once");
5051
const syncLoop = localSourceSyncCommand(runtime, "preview-loop");
5152
return [
5253
`${syncOnce} || exit $?`,
54+
`cd ${shellQuote(runtime.localCwd)} || exit $?`,
55+
...(prepareCommand ? [`${prepareCommand} || exit $?`] : []),
5356
`${syncLoop} &`,
5457
"sync_pid=$!",
55-
`cd ${shellQuote(runtime.localCwd)} || exit $?`,
5658
`${rawCommand} &`,
5759
"app_pid=$!",
5860
'terminate() { kill -TERM "$app_pid" "$sync_pid" 2>/dev/null || true; }',
@@ -64,3 +66,29 @@ export function localProjectProcessCommand(
6466
'exit "$status"',
6567
].join("\n");
6668
}
69+
70+
/** Reconstructs a disposable pnpm tree before a persisted process starts in a new container. */
71+
export function localPnpmProjectProcessCommand(
72+
runtime: ProjectLocalRuntime,
73+
rawCommand: string,
74+
dependencyTemplateDir?: string,
75+
): string {
76+
return localProjectProcessCommand(
77+
runtime,
78+
rawCommand,
79+
restorePnpmDependenciesCommand(dependencyTemplateDir),
80+
);
81+
}
82+
83+
function restorePnpmDependenciesCommand(dependencyTemplateDir?: string): string {
84+
const templateMatch = dependencyTemplateDir
85+
? `(cmp -s package.json ${shellQuote(`${dependencyTemplateDir}/package.json`)} && ` +
86+
`cmp -s pnpm-lock.yaml ${shellQuote(`${dependencyTemplateDir}/pnpm-lock.yaml`)})`
87+
: "false";
88+
const install =
89+
"if [ -f pnpm-lock.yaml ]; then " +
90+
"pnpm install --frozen-lockfile --offline || " +
91+
"pnpm install --frozen-lockfile --prefer-offline --network-concurrency 4; " +
92+
"else pnpm install --prefer-offline --network-concurrency 4; fi";
93+
return `if [ -f package.json ] && ! ${templateMatch}; then ` + `${install}; fi`;
94+
}

apps/agent-worker/src/durable-objects/project-sandbox-processes.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import type {
88
import type { SandboxConsoleSnapshot } from "@cheatcode/types/api";
99
import { sandboxExecProcessName } from "./project-sandbox-audit";
1010
import { WORKSPACE_DIR } from "./project-sandbox-content-support";
11-
import { localPackageCommand, localProjectProcessCommand } from "./project-sandbox-local-source";
11+
import {
12+
localPackageCommand,
13+
localPnpmProjectProcessCommand,
14+
} from "./project-sandbox-local-source";
1215
import { recordSandboxUsageBestEffort } from "./project-sandbox-metering";
1316
import {
1417
localizeProjectPackageCommand,
@@ -378,7 +381,7 @@ async function startProcess(
378381
? commandToShellString([
379382
"sh",
380383
"-lc",
381-
localProjectProcessCommand(
384+
localPnpmProjectProcessCommand(
382385
packageRuntime,
383386
commandToShellString(localizeProjectPackageCommand(packageRuntime, parsed.command)),
384387
),

packages/agent-core/src/mastra/tool-defs/code-tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export const mastraFsWrite = createTool({
156156
export const mastraFsApply = createTool({
157157
id: "fs_apply",
158158
description:
159-
"Apply a focused edit to an existing UTF-8 file under /workspace. Read the file first, then provide only changed code with // ... existing code ... wherever content stays unchanged. Use fs_write for new files, binary files, or intentional complete replacements.",
159+
"Edit an existing UTF-8 file under /workspace by showing only the changed lines. Read the file first. ALWAYS use the exact // ... existing code ... marker for every unchanged section; omitting it deletes that section. Preserve indentation, include only enough surrounding context to locate each edit, and batch multiple changes to the same file in one call. Use fs_write only for new files, binary files, or intentional complete replacements.",
160160
inputSchema: ApplyFileInputSchema,
161161
outputSchema: ApplyFileOutputSchema,
162162
execute: async (input, context) => {

packages/agent-core/src/tools/code/files.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ export const ApplyFileInputSchema = z.strictObject({
5656
(value) => value.includes(EXISTING_CODE_MARKER),
5757
`Sparse edits must include ${EXISTING_CODE_MARKER}`,
5858
)
59-
.describe("Only changed code with // ... existing code ... marking unchanged regions."),
59+
.describe(
60+
"Only the changed lines, with the exact // ... existing code ... marker for every unchanged region. Preserve indentation and include only enough surrounding context to locate each edit.",
61+
),
6062
});
6163

6264
export const ApplyFileOutputSchema = z.strictObject({

0 commit comments

Comments
 (0)