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
11 changes: 11 additions & 0 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ before executing against a user-connected OAuth account.
ProjectSandbox records elapsed sandbox-hours to the same `QuotaTracker` as a soft
meter so Settings can show real monthly sandbox consumption without blocking
sandbox file/process work.

Opening Files admits code-server startup durably instead of holding the browser request open while
Daytona starts. ProjectSandbox stores one generation-checked startup operation, schedules it through
the alarm shared with run keepalive, and reports bounded queued, sandbox-starting, files-starting,
ready, or failed status. Alarm handling runs keepalive first so every retry leaves the next lease
refresh scheduled; a retriable Files failure then moves that alarm forward to its earlier retry.
At-least-once delivery is safe because every completion write compares the operation generation and
code-server keeps its deterministic process identity. The legacy session GET remains synchronous for
rollout compatibility, while a fresh ready record tied to the current Daytona sandbox lets the normal
path mint its short-lived capability without re-probing the container. Preview-origin credentials
remain owned by the preview proxy and are never persisted in the startup record.
Lazy workspace materialization resolves the effective project ceiling from the authoritative
entitlement row, including an operator-granted override when present, before creating a project.

Expand Down
10 changes: 9 additions & 1 deletion apps/agent-worker/src/durable-objects/agent-run-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ async function resolveWorkspace(input: WorkspaceResolverInput): Promise<Workspac
input.input.workspaceSlug = project.workspaceSlug;
if (result.kind === "created") {
await input.append({
data: { projectId: project.id, projectName: project.name, v: 1 },
data: {
projectId: project.id,
projectMode: project.mode,
projectName: project.name,
previewBuilding:
(project.mode === "app-builder" || project.mode === "app-builder-mobile") &&
!input.input.importRepoUrl,
v: 1,
},
type: "data-project-created",
});
}
Expand Down
86 changes: 86 additions & 0 deletions apps/agent-worker/src/durable-objects/project-sandbox-alarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const CODE_SERVER_DEADLINE_KEY = "project_sandbox_code_server_alarm_at";
const KEEPALIVE_DEADLINE_KEY = "project_sandbox_keepalive_alarm_at";
const ALARM_DUE_TOLERANCE_MS = 1_000;

export interface ProjectSandboxAlarmTasks {
codeServer: boolean;
keepalive: boolean;
}

export function scheduleCodeServerAlarm(
storage: DurableObjectStorage,
scheduledAtMs: number | null,
): Promise<void> {
return updateAlarmDeadline(storage, CODE_SERVER_DEADLINE_KEY, scheduledAtMs);
}

export function scheduleKeepaliveAlarm(
storage: DurableObjectStorage,
scheduledAtMs: number | null,
): Promise<void> {
return updateAlarmDeadline(storage, KEEPALIVE_DEADLINE_KEY, scheduledAtMs);
}

export async function projectSandboxAlarmTasksDue(
storage: DurableObjectStorage,
): Promise<ProjectSandboxAlarmTasks> {
const [codeServerAt, keepaliveAt] = await Promise.all([
storage.get<number>(CODE_SERVER_DEADLINE_KEY),
storage.get<number>(KEEPALIVE_DEADLINE_KEY),
]);
const dueAt = Date.now() + ALARM_DUE_TOLERANCE_MS;
// A pre-multiplexer deployment may already have a keepalive alarm but no
// deadline keys. Treat that one alarm as keepalive work and migrate in place.
const isLegacyAlarm = codeServerAt === undefined && keepaliveAt === undefined;
return {
codeServer: codeServerAt !== undefined && codeServerAt <= dueAt,
keepalive: isLegacyAlarm || (keepaliveAt !== undefined && keepaliveAt <= dueAt),
};
}

export function rescheduleProjectSandboxAlarm(storage: DurableObjectStorage): Promise<void> {
return storage.transaction((transaction) => applyEarliestAlarm(transaction));
}

function updateAlarmDeadline(
storage: DurableObjectStorage,
key: string,
scheduledAtMs: number | null,
): Promise<void> {
return storage.transaction(async (transaction) => {
await adoptLegacyKeepaliveAlarm(transaction);
if (scheduledAtMs === null) {
await transaction.delete(key);
} else {
await transaction.put(key, scheduledAtMs);
}
await applyEarliestAlarm(transaction);
});
}

async function adoptLegacyKeepaliveAlarm(transaction: DurableObjectTransaction): Promise<void> {
const [codeServerAt, keepaliveAt] = await Promise.all([
transaction.get<number>(CODE_SERVER_DEADLINE_KEY),
transaction.get<number>(KEEPALIVE_DEADLINE_KEY),
]);
if (codeServerAt !== undefined || keepaliveAt !== undefined) return;
const legacyAlarmAt = await transaction.getAlarm();
if (legacyAlarmAt !== null) {
await transaction.put(KEEPALIVE_DEADLINE_KEY, legacyAlarmAt);
}
}

async function applyEarliestAlarm(transaction: DurableObjectTransaction): Promise<void> {
const [codeServerAt, keepaliveAt] = await Promise.all([
transaction.get<number>(CODE_SERVER_DEADLINE_KEY),
transaction.get<number>(KEEPALIVE_DEADLINE_KEY),
]);
const deadlines = [codeServerAt, keepaliveAt].filter(
(value): value is number => value !== undefined,
);
if (deadlines.length === 0) {
await transaction.deleteAlarm();
return;
}
await transaction.setAlarm(Math.min(...deadlines));
}
Loading