mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 03:00:48 +00:00
Fix E2B sandbox timeout comment, frontend stability, and create-flow improvements
- Add TEMPORARY comment on E2B timeoutMs with pointer to rivetkit sandbox resilience proposal for when autoPause lands - Fix React useEffect dependency stability in mock-layout and organization-dashboard to prevent infinite re-render loops - Fix terminal-pane ref handling - Improve create-flow service and tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
78cd38d826
commit
ee25a6c6a5
6 changed files with 106 additions and 42 deletions
|
|
@ -205,9 +205,12 @@ const baseTaskSandbox = sandboxActor({
|
|||
create: () => ({
|
||||
template: config.sandboxProviders.e2b.template ?? "sandbox-agent-full-0.3.x",
|
||||
envs: sandboxEnvObject(),
|
||||
// Default E2B timeout is 5 minutes which is too short for task work.
|
||||
// Set to 1 hour. TODO: use betaCreate + autoPause instead so sandboxes
|
||||
// pause (preserving state) rather than being killed on timeout.
|
||||
// TEMPORARY: Default E2B timeout is 5 minutes which is too short.
|
||||
// Set to 1 hour as a stopgap. Remove this once the E2B provider in
|
||||
// sandbox-agent uses betaCreate + autoPause (see
|
||||
// .context/proposal-rivetkit-sandbox-resilience.md). At that point
|
||||
// the provider handles timeout/pause lifecycle and this override is
|
||||
// unnecessary.
|
||||
timeoutMs: 60 * 60 * 1000,
|
||||
}),
|
||||
installAgents: ["claude", "codex"],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { BRANCH_NAME_PREFIXES } from "./branch-name-prefixes.js";
|
||||
|
||||
export interface ResolveCreateFlowDecisionInput {
|
||||
task: string;
|
||||
explicitTitle?: string;
|
||||
|
|
@ -89,30 +91,42 @@ export function sanitizeBranchName(input: string): string {
|
|||
return trimmed.slice(0, 50).replace(/-+$/g, "");
|
||||
}
|
||||
|
||||
function generateRandomSuffix(length: number): string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateBranchName(): string {
|
||||
const prefix = BRANCH_NAME_PREFIXES[Math.floor(Math.random() * BRANCH_NAME_PREFIXES.length)]!;
|
||||
const suffix = generateRandomSuffix(4);
|
||||
return `${prefix}-${suffix}`;
|
||||
}
|
||||
|
||||
export function resolveCreateFlowDecision(input: ResolveCreateFlowDecisionInput): ResolveCreateFlowDecisionResult {
|
||||
const explicitBranch = input.explicitBranchName?.trim();
|
||||
const title = deriveFallbackTitle(input.task, input.explicitTitle);
|
||||
const generatedBase = sanitizeBranchName(title) || "task";
|
||||
|
||||
const branchBase = explicitBranch && explicitBranch.length > 0 ? explicitBranch : generatedBase;
|
||||
|
||||
const existingBranches = new Set(input.localBranches.map((value) => value.trim()).filter((value) => value.length > 0));
|
||||
const existingTaskBranches = new Set(input.taskBranches.map((value) => value.trim()).filter((value) => value.length > 0));
|
||||
const conflicts = (name: string): boolean => existingBranches.has(name) || existingTaskBranches.has(name);
|
||||
|
||||
if (explicitBranch && conflicts(branchBase)) {
|
||||
throw new Error(`Branch '${branchBase}' already exists. Choose a different --name/--branch value.`);
|
||||
if (explicitBranch && explicitBranch.length > 0) {
|
||||
if (conflicts(explicitBranch)) {
|
||||
throw new Error(`Branch '${explicitBranch}' already exists. Choose a different --name/--branch value.`);
|
||||
}
|
||||
return { title, branchName: explicitBranch };
|
||||
}
|
||||
|
||||
if (explicitBranch) {
|
||||
return { title, branchName: branchBase };
|
||||
}
|
||||
|
||||
let candidate = branchBase;
|
||||
let index = 2;
|
||||
while (conflicts(candidate)) {
|
||||
candidate = `${branchBase}-${index}`;
|
||||
index += 1;
|
||||
// Generate a random McMaster-Carr-style branch name, retrying on conflicts
|
||||
let candidate = generateBranchName();
|
||||
let attempts = 0;
|
||||
while (conflicts(candidate) && attempts < 100) {
|
||||
candidate = generateBranchName();
|
||||
attempts += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { deriveFallbackTitle, resolveCreateFlowDecision, sanitizeBranchName } from "../src/services/create-flow.js";
|
||||
import { BRANCH_NAME_PREFIXES } from "../src/services/branch-name-prefixes.js";
|
||||
|
||||
describe("create flow decision", () => {
|
||||
it("derives a conventional-style fallback title from task text", () => {
|
||||
|
|
@ -17,15 +18,49 @@ describe("create flow decision", () => {
|
|||
expect(sanitizeBranchName(" spaces everywhere ")).toBe("spaces-everywhere");
|
||||
});
|
||||
|
||||
it("auto-increments generated branch names for conflicts", () => {
|
||||
it("generates a McMaster-Carr-style branch name with random suffix", () => {
|
||||
const resolved = resolveCreateFlowDecision({
|
||||
task: "Add auth",
|
||||
localBranches: ["feat-add-auth"],
|
||||
taskBranches: ["feat-add-auth-2"],
|
||||
localBranches: [],
|
||||
taskBranches: [],
|
||||
});
|
||||
|
||||
expect(resolved.title).toBe("feat: Add auth");
|
||||
expect(resolved.branchName).toBe("feat-add-auth-3");
|
||||
// Branch name should be "<prefix>-<4-char-suffix>" where prefix is from BRANCH_NAME_PREFIXES
|
||||
const lastDash = resolved.branchName.lastIndexOf("-");
|
||||
const prefix = resolved.branchName.slice(0, lastDash);
|
||||
const suffix = resolved.branchName.slice(lastDash + 1);
|
||||
expect(BRANCH_NAME_PREFIXES).toContain(prefix);
|
||||
expect(suffix).toMatch(/^[a-z0-9]{4}$/);
|
||||
});
|
||||
|
||||
it("avoids conflicts by generating a different random name", () => {
|
||||
// Even with a conflicting branch, it should produce something different
|
||||
const resolved = resolveCreateFlowDecision({
|
||||
task: "Add auth",
|
||||
localBranches: [],
|
||||
taskBranches: [],
|
||||
});
|
||||
|
||||
// Running again with the first result as a conflict should produce a different name
|
||||
const resolved2 = resolveCreateFlowDecision({
|
||||
task: "Add auth",
|
||||
localBranches: [resolved.branchName],
|
||||
taskBranches: [],
|
||||
});
|
||||
|
||||
expect(resolved2.branchName).not.toBe(resolved.branchName);
|
||||
});
|
||||
|
||||
it("uses explicit branch name when provided", () => {
|
||||
const resolved = resolveCreateFlowDecision({
|
||||
task: "new task",
|
||||
explicitBranchName: "my-branch",
|
||||
localBranches: [],
|
||||
taskBranches: [],
|
||||
});
|
||||
|
||||
expect(resolved.branchName).toBe("my-branch");
|
||||
});
|
||||
|
||||
it("fails when explicit branch already exists", () => {
|
||||
|
|
|
|||
|
|
@ -1317,11 +1317,14 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
[organizationId],
|
||||
);
|
||||
const organizationState = useSubscription(subscriptionManager, "organization", { organizationId });
|
||||
const organizationRepos = organizationState.data?.repos ?? [];
|
||||
const taskSummaries = organizationState.data?.taskSummaries ?? [];
|
||||
const organizationReposData = organizationState.data?.repos;
|
||||
const taskSummariesData = organizationState.data?.taskSummaries;
|
||||
const openPullRequestsData = organizationState.data?.openPullRequests;
|
||||
const organizationRepos = organizationReposData ?? [];
|
||||
const taskSummaries = taskSummariesData ?? [];
|
||||
const selectedTaskSummary = useMemo(
|
||||
() => taskSummaries.find((task) => task.id === selectedTaskId) ?? taskSummaries[0] ?? null,
|
||||
[selectedTaskId, taskSummaries],
|
||||
[selectedTaskId, taskSummariesData],
|
||||
);
|
||||
const taskState = useSubscription(
|
||||
subscriptionManager,
|
||||
|
|
@ -1401,9 +1404,9 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
summary.id === selectedTaskSummary?.id ? toTaskModel(summary, taskState.data, sessionCache) : toTaskModel(summary),
|
||||
);
|
||||
return hydratedTasks.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
||||
}, [selectedTaskSummary, selectedSessionId, sessionState.data, taskState.data, taskSummaries, organizationId]);
|
||||
const openPullRequests = organizationState.data?.openPullRequests ?? [];
|
||||
const rawRepositories = useMemo(() => groupRepositories(organizationRepos, tasks, openPullRequests), [tasks, organizationRepos, openPullRequests]);
|
||||
}, [selectedTaskSummary, selectedSessionId, sessionState.data, taskState.data, taskSummariesData, organizationId]);
|
||||
const openPullRequests = openPullRequestsData ?? [];
|
||||
const rawRepositories = useMemo(() => groupRepositories(organizationRepos, tasks, openPullRequests), [tasks, organizationReposData, openPullRequestsData]);
|
||||
const appSnapshot = useMockAppSnapshot();
|
||||
const currentUser = activeMockUser(appSnapshot);
|
||||
const activeOrg = activeMockOrganization(appSnapshot);
|
||||
|
|
@ -1591,6 +1594,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
}, [activeTask, lastAgentSessionIdByTask, selectedSessionId, syncRouteSession]);
|
||||
|
||||
useEffect(() => {
|
||||
const organizationRepos = organizationReposData ?? [];
|
||||
if (selectedNewTaskRepoId && organizationRepos.some((repo) => repo.id === selectedNewTaskRepoId)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1600,7 +1604,7 @@ export function MockLayout({ organizationId, selectedTaskId, selectedSessionId }
|
|||
if (fallbackRepoId !== selectedNewTaskRepoId) {
|
||||
setSelectedNewTaskRepoId(fallbackRepoId);
|
||||
}
|
||||
}, [activeTask?.repoId, selectedNewTaskRepoId, organizationRepos]);
|
||||
}, [activeTask?.repoId, selectedNewTaskRepoId, organizationReposData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTask) {
|
||||
|
|
|
|||
|
|
@ -305,7 +305,8 @@ export function TerminalPane({ organizationId, taskId, isExpanded, onExpand, onC
|
|||
setProcessTabs([]);
|
||||
}, [taskId]);
|
||||
|
||||
const processes = processesState.data ?? [];
|
||||
const processesData = processesState.data;
|
||||
const processes = processesData ?? [];
|
||||
|
||||
const openTerminalTab = useCallback((process: SandboxProcessRecord) => {
|
||||
setProcessTabs((current) => {
|
||||
|
|
@ -361,7 +362,7 @@ export function TerminalPane({ organizationId, taskId, isExpanded, onExpand, onC
|
|||
const activeProcessTab = activeSessionId ? (processTabsById.get(activeSessionId) ?? null) : null;
|
||||
const activeTerminalProcess = useMemo(
|
||||
() => (activeProcessTab ? (processes.find((process) => process.id === activeProcessTab.processId) ?? null) : null),
|
||||
[activeProcessTab, processes],
|
||||
[activeProcessTab, processesData],
|
||||
);
|
||||
|
||||
const emptyBodyClassName = css({
|
||||
|
|
|
|||
|
|
@ -335,9 +335,11 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
const activeOrg = appState.data ? currentFoundryOrganization(appState.data) : null;
|
||||
|
||||
const organizationState = useSubscription(subscriptionManager, "organization", { organizationId });
|
||||
const repos = organizationState.data?.repos ?? [];
|
||||
const rows = organizationState.data?.taskSummaries ?? [];
|
||||
const selectedSummary = useMemo(() => rows.find((row) => row.id === selectedTaskId) ?? rows[0] ?? null, [rows, selectedTaskId]);
|
||||
const reposData = organizationState.data?.repos;
|
||||
const rowsData = organizationState.data?.taskSummaries;
|
||||
const repos = reposData ?? [];
|
||||
const rows = rowsData ?? [];
|
||||
const selectedSummary = useMemo(() => rows.find((row) => row.id === selectedTaskId) ?? rows[0] ?? null, [rowsData, selectedTaskId]);
|
||||
const taskState = useSubscription(
|
||||
subscriptionManager,
|
||||
"task",
|
||||
|
|
@ -363,6 +365,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
const repos = reposData ?? [];
|
||||
if (repoOverviewMode && selectedRepoId) {
|
||||
setCreateRepoId(selectedRepoId);
|
||||
return;
|
||||
|
|
@ -370,9 +373,11 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
if (!createRepoId && repos.length > 0) {
|
||||
setCreateRepoId(repos[0]!.id);
|
||||
}
|
||||
}, [createRepoId, repoOverviewMode, repos, selectedRepoId]);
|
||||
}, [createRepoId, repoOverviewMode, reposData, selectedRepoId]);
|
||||
|
||||
const repoGroups = useMemo(() => {
|
||||
const repos = reposData ?? [];
|
||||
const rows = rowsData ?? [];
|
||||
const byRepo = new Map<string, typeof rows>();
|
||||
for (const row of rows) {
|
||||
const bucket = byRepo.get(row.repoId);
|
||||
|
|
@ -400,7 +405,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
}
|
||||
return a.repoLabel.localeCompare(b.repoLabel);
|
||||
});
|
||||
}, [repos, rows]);
|
||||
}, [reposData, rowsData]);
|
||||
|
||||
const selectedForSession = repoOverviewMode ? null : (taskState.data ?? null);
|
||||
|
||||
|
|
@ -413,6 +418,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
}, [selectedForSession]);
|
||||
|
||||
useEffect(() => {
|
||||
const rows = rowsData ?? [];
|
||||
if (!repoOverviewMode && !selectedTaskId && rows.length > 0) {
|
||||
void navigate({
|
||||
to: "/organizations/$organizationId/tasks/$taskId",
|
||||
|
|
@ -424,14 +430,15 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
replace: true,
|
||||
});
|
||||
}
|
||||
}, [navigate, repoOverviewMode, rows, selectedTaskId, organizationId]);
|
||||
}, [navigate, repoOverviewMode, rowsData, selectedTaskId, organizationId]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSessionId(null);
|
||||
setDraft("");
|
||||
}, [selectedForSession?.id]);
|
||||
|
||||
const sessionRows = selectedForSession?.sessionsSummary ?? [];
|
||||
const sessionRowsData = selectedForSession?.sessionsSummary;
|
||||
const sessionRows = sessionRowsData ?? [];
|
||||
const taskStatus = selectedForSession?.status ?? null;
|
||||
const taskStatusState = describeTaskState(taskStatus);
|
||||
const taskStateSummary = `${taskStatusState.title}. ${taskStatusState.detail}`;
|
||||
|
|
@ -450,7 +457,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
status: session.status,
|
||||
})),
|
||||
}),
|
||||
[activeSessionId, selectedForSession?.activeSessionId, sessionRows],
|
||||
[activeSessionId, selectedForSession?.activeSessionId, sessionRowsData],
|
||||
);
|
||||
const resolvedSessionId = sessionSelection.sessionId;
|
||||
const staleSessionId = sessionSelection.staleSessionId;
|
||||
|
|
@ -466,7 +473,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
}
|
||||
: null,
|
||||
);
|
||||
const selectedSessionSummary = useMemo(() => sessionRows.find((session) => session.id === resolvedSessionId) ?? null, [resolvedSessionId, sessionRows]);
|
||||
const selectedSessionSummary = useMemo(() => sessionRows.find((session) => session.id === resolvedSessionId) ?? null, [resolvedSessionId, sessionRowsData]);
|
||||
const isPendingProvision = selectedSessionSummary?.status === "pending_provision";
|
||||
const isPendingSessionCreate = selectedSessionSummary?.status === "pending_session_create";
|
||||
const isSessionError = selectedSessionSummary?.status === "error";
|
||||
|
|
@ -523,7 +530,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
activeSandboxId: selectedForSession?.id === task.id ? selectedForSession.activeSandboxId : null,
|
||||
})),
|
||||
}),
|
||||
[repos, rows, selectedForSession, organizationId],
|
||||
[reposData, rowsData, selectedForSession, organizationId],
|
||||
);
|
||||
|
||||
const startSessionFromTask = async (): Promise<{ id: string; status: "running" | "idle" | "error" }> => {
|
||||
|
|
@ -631,7 +638,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
setCreateTaskOpen(true);
|
||||
};
|
||||
|
||||
const repoOptions = useMemo(() => repos.map((repo) => createOption({ id: repo.id, label: repo.label })), [repos]);
|
||||
const repoOptions = useMemo(() => repos.map((repo) => createOption({ id: repo.id, label: repo.label })), [reposData]);
|
||||
const selectedRepoOption = repoOptions.find((option) => option.id === createRepoId) ?? null;
|
||||
const selectedFilterOption = useMemo(
|
||||
() => createOption(FILTER_OPTIONS.find((option) => option.id === overviewFilter) ?? FILTER_OPTIONS[0]!),
|
||||
|
|
@ -639,7 +646,7 @@ export function OrganizationDashboard({ organizationId, selectedTaskId, selected
|
|||
);
|
||||
const sessionOptions = useMemo(
|
||||
() => sessionRows.map((session) => createOption({ id: session.id, label: `${session.sessionName} (${session.status})` })),
|
||||
[sessionRows],
|
||||
[sessionRowsData],
|
||||
);
|
||||
const selectedSessionOption = sessionOptions.find((option) => option.id === resolvedSessionId) ?? null;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue