Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ webpack compilation even after the listening socket opens. A baked Python synchr
full refresh on every process start and mirrors subsequent writes and deletions within one second,
including shell-based edits. The local source, dependency tree, and build cache are disposable;
wake and restart reconstruct them from the durable project without changing the Files surface.
Direct pnpm commands use that same native-disk source as a transaction: the runtime snapshots the
durable source, executes pnpm locally, and copies only command-produced source changes back after
verifying that the corresponding durable paths did not change concurrently. Long-running pnpm
processes keep the durable-to-local mirror alive for hot reload. Shell-wrapped package managers are
rejected because they cannot participate in this synchronization boundary; npm, Yarn, and Bun stay
disabled for project workspaces.

AgentRun keeps one compact exact SQLite shape for run identity, replay parts, and
coordination state. Dormant objects are reconciled transactionally on activation;
Expand Down
105 changes: 7 additions & 98 deletions apps/agent-worker/src/durable-objects/app-builder-local-preview.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,19 @@
import { shellQuote } from "../sandbox-support";
import { NEXT_RUNTIME_BIN, projectLocalSourceDir } from "./project-sandbox-package-runtime";
import { localProjectProcessCommand } from "./project-sandbox-local-source";
import { NEXT_RUNTIME_BIN, resolveProjectLocalRuntime } from "./project-sandbox-package-runtime";

interface LocalPreviewCommandInput {
port: number;
sourceDir: string;
workspaceSlug: string;
}

const PREVIEW_MIRROR_SYNC_SCRIPT = `
import os
import shutil
import sys
import time

IGNORED_DIRS = {".expo", ".next", "node_modules"}
LOCAL_ONLY_NAMES = IGNORED_DIRS | {"next-env.d.ts"}

def remove_path(path):
if os.path.isdir(path) and not os.path.islink(path):
shutil.rmtree(path)
elif os.path.lexists(path):
os.unlink(path)

def sync_link(source, target):
link = os.readlink(source)
if os.path.islink(target) and os.readlink(target) == link:
return
remove_path(target)
os.symlink(link, target)

def sync_file(entry, target, force):
source_stat = entry.stat(follow_symlinks=False)
try:
target_stat = os.stat(target, follow_symlinks=False)
except FileNotFoundError:
target_stat = None
changed = (
force
or target_stat is None
or not os.path.isfile(target)
or source_stat.st_size != target_stat.st_size
or source_stat.st_mtime_ns > target_stat.st_mtime_ns
)
if changed:
remove_path(target)
shutil.copy2(entry.path, target, follow_symlinks=False)

def sync_directory(source, target, force=False):
if os.path.lexists(target) and not os.path.isdir(target):
remove_path(target)
os.makedirs(target, exist_ok=True)
source_names = set()
for entry in os.scandir(source):
if entry.name in IGNORED_DIRS:
continue
source_names.add(entry.name)
destination = os.path.join(target, entry.name)
try:
if entry.is_symlink():
sync_link(entry.path, destination)
elif entry.is_dir(follow_symlinks=False):
sync_directory(entry.path, destination, force)
elif entry.is_file(follow_symlinks=False):
sync_file(entry, destination, force)
except FileNotFoundError:
continue
for entry in os.scandir(target):
if entry.name not in source_names and entry.name not in LOCAL_ONLY_NAMES:
remove_path(entry.path)

def synchronize(source, target, force):
sync_directory(source, target, force)

source_dir, target_dir, mode = sys.argv[1:4]
if mode == "once":
synchronize(source_dir, target_dir, True)
else:
while True:
try:
synchronize(source_dir, target_dir, False)
except OSError:
pass
time.sleep(0.75)
`;

/** Runs Next from native sandbox disk while `/workspace` remains the durable project source. */
export function localNextPreviewCommand(input: LocalPreviewCommandInput): string[] {
const localSourceDir = projectLocalSourceDir(input.workspaceSlug);
const syncCommand = ["python3", "-c", PREVIEW_MIRROR_SYNC_SCRIPT, input.sourceDir, localSourceDir]
.map(shellQuote)
.join(" ");
const runtime = resolveProjectLocalRuntime(input.sourceDir);
if (!runtime || runtime.workspaceSlug !== input.workspaceSlug) {
throw new TypeError("Preview source does not match its workspace slug.");
}
const nextCommand = [
NEXT_RUNTIME_BIN,
"dev",
Expand All @@ -101,20 +25,5 @@ export function localNextPreviewCommand(input: LocalPreviewCommandInput): string
]
.map(shellQuote)
.join(" ");
const command = [
`${syncCommand} once || exit $?`,
`${syncCommand} loop &`,
"sync_pid=$!",
`cd ${shellQuote(localSourceDir)} || exit $?`,
`${nextCommand} &`,
"app_pid=$!",
'terminate() { kill -TERM "$app_pid" "$sync_pid" 2>/dev/null || true; }',
"trap terminate HUP INT TERM",
'wait "$app_pid"',
"status=$?",
'kill "$sync_pid" 2>/dev/null || true',
'wait "$sync_pid" 2>/dev/null || true',
'exit "$status"',
].join("\n");
return ["sh", "-lc", command];
return ["sh", "-lc", localProjectProcessCommand(runtime, nextCommand)];
}
Loading