Merge remote-tracking branch 'origin/main' into foundry-terminal-pane

# Conflicts:
#	factory/packages/backend/src/driver.ts
#	factory/packages/backend/src/integrations/sandbox-agent/client.ts
#	factory/packages/backend/test/helpers/test-driver.ts
#	factory/packages/frontend/src/components/mock-layout.tsx
#	pnpm-lock.yaml
#	sdks/react/src/ProcessTerminal.tsx
This commit is contained in:
Nathan Flurry 2026-03-10 23:59:58 -07:00
commit b00c0109d0
288 changed files with 7048 additions and 9134 deletions

View file

@ -36,9 +36,7 @@ const CREATE_SESSION_MAX_ATTEMPTS = 3;
const CREATE_SESSION_RETRY_BASE_MS = 1_000;
const CREATE_SESSION_STEP_TIMEOUT_MS = 10 * 60_000;
function normalizeStatusFromEventPayload(
payload: unknown,
): "running" | "idle" | "error" | null {
function normalizeStatusFromEventPayload(payload: unknown): "running" | "idle" | "error" | null {
if (payload && typeof payload === "object") {
const envelope = payload as {
error?: unknown;
@ -62,11 +60,7 @@ function normalizeStatusFromEventPayload(
if (lowered.includes("error") || lowered.includes("failed")) {
return "error";
}
if (
lowered.includes("ended") ||
lowered.includes("complete") ||
lowered.includes("stopped")
) {
if (lowered.includes("ended") || lowered.includes("complete") || lowered.includes("stopped")) {
return "idle";
}
}
@ -196,12 +190,7 @@ async function derivePersistedSessionStatus(
function isTransientSessionCreateError(detail: string): boolean {
const lowered = detail.toLowerCase();
if (
lowered.includes("timed out") ||
lowered.includes("timeout") ||
lowered.includes("504") ||
lowered.includes("gateway timeout")
) {
if (lowered.includes("timed out") || lowered.includes("timeout") || lowered.includes("504") || lowered.includes("gateway timeout")) {
// ACP timeout errors are expensive and usually deterministic for the same
// request; immediate retries spawn additional sessions/processes and make
// recovery harder.
@ -209,11 +198,7 @@ function isTransientSessionCreateError(detail: string): boolean {
}
return (
lowered.includes("502") ||
lowered.includes("503") ||
lowered.includes("bad gateway") ||
lowered.includes("econnreset") ||
lowered.includes("econnrefused")
lowered.includes("502") || lowered.includes("503") || lowered.includes("bad gateway") || lowered.includes("econnreset") || lowered.includes("econnrefused")
);
}
@ -278,9 +263,7 @@ const SANDBOX_INSTANCE_QUEUE_NAMES = [
type SandboxInstanceQueueName = (typeof SANDBOX_INSTANCE_QUEUE_NAMES)[number];
function sandboxInstanceWorkflowQueueName(
name: SandboxInstanceQueueName,
): SandboxInstanceQueueName {
function sandboxInstanceWorkflowQueueName(name: SandboxInstanceQueueName): SandboxInstanceQueueName {
return name;
}
@ -317,15 +300,15 @@ async function ensureSandboxMutation(c: any, command: EnsureSandboxCommand): Pro
id: SANDBOX_ROW_ID,
metadataJson,
status: command.status,
updatedAt: now
updatedAt: now,
})
.onConflictDoUpdate({
target: sandboxInstanceTable.id,
set: {
metadataJson,
status: command.status,
updatedAt: now
}
updatedAt: now,
},
})
.run();
}
@ -335,17 +318,14 @@ async function updateHealthMutation(c: any, command: HealthSandboxCommand): Prom
.update(sandboxInstanceTable)
.set({
status: `${command.status}:${command.message}`,
updatedAt: Date.now()
updatedAt: Date.now(),
})
.where(eq(sandboxInstanceTable.id, SANDBOX_ROW_ID))
.run();
}
async function destroySandboxMutation(c: any): Promise<void> {
await c.db
.delete(sandboxInstanceTable)
.where(eq(sandboxInstanceTable.id, SANDBOX_ROW_ID))
.run();
await c.db.delete(sandboxInstanceTable).where(eq(sandboxInstanceTable.id, SANDBOX_ROW_ID)).run();
}
async function createSessionMutation(c: any, command: CreateSessionCommand): Promise<CreateSessionResult> {
@ -382,7 +362,7 @@ async function createSessionMutation(c: any, command: CreateSessionCommand): Pro
attempt,
maxAttempts: CREATE_SESSION_MAX_ATTEMPTS,
waitMs,
error: detail
error: detail,
});
await delay(waitMs);
}
@ -392,7 +372,7 @@ async function createSessionMutation(c: any, command: CreateSessionCommand): Pro
return {
id: null,
status: "error",
error: `sandbox-agent createSession failed after ${attemptsMade} ${attemptLabel}: ${lastDetail}`
error: `sandbox-agent createSession failed after ${attemptsMade} ${attemptLabel}: ${lastDetail}`,
};
}
@ -425,62 +405,50 @@ async function runSandboxInstanceWorkflow(ctx: any): Promise<void> {
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.ensure") {
await loopCtx.step("sandbox-instance-ensure", async () =>
ensureSandboxMutation(loopCtx, msg.body as EnsureSandboxCommand),
);
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.ensure") {
await loopCtx.step("sandbox-instance-ensure", async () => ensureSandboxMutation(loopCtx, msg.body as EnsureSandboxCommand));
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.updateHealth") {
await loopCtx.step("sandbox-instance-update-health", async () =>
updateHealthMutation(loopCtx, msg.body as HealthSandboxCommand),
);
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.updateHealth") {
await loopCtx.step("sandbox-instance-update-health", async () => updateHealthMutation(loopCtx, msg.body as HealthSandboxCommand));
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.destroy") {
await loopCtx.step("sandbox-instance-destroy", async () =>
destroySandboxMutation(loopCtx),
);
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.destroy") {
await loopCtx.step("sandbox-instance-destroy", async () => destroySandboxMutation(loopCtx));
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.createSession") {
const result = await loopCtx.step({
name: "sandbox-instance-create-session",
timeout: CREATE_SESSION_STEP_TIMEOUT_MS,
run: async () => createSessionMutation(loopCtx, msg.body as CreateSessionCommand),
});
await msg.complete(result);
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.createSession") {
const result = await loopCtx.step({
name: "sandbox-instance-create-session",
timeout: CREATE_SESSION_STEP_TIMEOUT_MS,
run: async () => createSessionMutation(loopCtx, msg.body as CreateSessionCommand),
});
await msg.complete(result);
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.sendPrompt") {
await loopCtx.step("sandbox-instance-send-prompt", async () =>
sendPromptMutation(loopCtx, msg.body as SendPromptCommand),
);
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.sendPrompt") {
await loopCtx.step("sandbox-instance-send-prompt", async () => sendPromptMutation(loopCtx, msg.body as SendPromptCommand));
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.cancelSession") {
await loopCtx.step("sandbox-instance-cancel-session", async () =>
cancelSessionMutation(loopCtx, msg.body as SessionControlCommand),
);
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.cancelSession") {
await loopCtx.step("sandbox-instance-cancel-session", async () => cancelSessionMutation(loopCtx, msg.body as SessionControlCommand));
await msg.complete({ ok: true });
return Loop.continue(undefined);
}
if (msg.name === "sandboxInstance.command.destroySession") {
await loopCtx.step("sandbox-instance-destroy-session", async () =>
destroySessionMutation(loopCtx, msg.body as SessionControlCommand),
);
await msg.complete({ ok: true });
}
if (msg.name === "sandboxInstance.command.destroySession") {
await loopCtx.step("sandbox-instance-destroy-session", async () => destroySessionMutation(loopCtx, msg.body as SessionControlCommand));
await msg.complete({ ok: true });
}
return Loop.continue(undefined);
});
@ -588,10 +556,14 @@ export const sandboxInstance = actor({
async destroy(c): Promise<void> {
const self = selfSandboxInstance(c);
await self.send(sandboxInstanceWorkflowQueueName("sandboxInstance.command.destroy"), {}, {
wait: true,
timeout: 60_000,
});
await self.send(
sandboxInstanceWorkflowQueueName("sandboxInstance.command.destroy"),
{},
{
wait: true,
timeout: 60_000,
},
);
},
async createSession(c: any, command: CreateSessionCommand): Promise<CreateSessionResult> {
@ -604,10 +576,7 @@ export const sandboxInstance = actor({
);
},
async listSessions(
c: any,
command?: ListSessionsCommand
): Promise<{ items: SessionRecord[]; nextCursor?: string }> {
async listSessions(c: any, command?: ListSessionsCommand): Promise<{ items: SessionRecord[]; nextCursor?: string }> {
const persist = new SandboxInstancePersistDriver(c.db);
try {
const client = await getSandboxAgentClient(c);
@ -626,7 +595,7 @@ export const sandboxInstance = actor({
workspaceId: c.state.workspaceId,
providerId: c.state.providerId,
sandboxId: c.state.sandboxId,
error: resolveErrorMessage(error)
error: resolveErrorMessage(error),
});
return await persist.listSessions({
cursor: command?.cursor,
@ -635,10 +604,7 @@ export const sandboxInstance = actor({
}
},
async listSessionEvents(
c: any,
command: ListSessionEventsCommand
): Promise<{ items: SessionEvent[]; nextCursor?: string }> {
async listSessionEvents(c: any, command: ListSessionEventsCommand): Promise<{ items: SessionEvent[]; nextCursor?: string }> {
const persist = new SandboxInstancePersistDriver(c.db);
return await persist.listEvents({
sessionId: command.sessionId,
@ -671,15 +637,9 @@ export const sandboxInstance = actor({
});
},
async sessionStatus(
c,
command: SessionStatusCommand
): Promise<{ id: string; status: "running" | "idle" | "error" }> {
return await derivePersistedSessionStatus(
new SandboxInstancePersistDriver(c.db),
command.sessionId,
);
}
async sessionStatus(c, command: SessionStatusCommand): Promise<{ id: string; status: "running" | "idle" | "error" }> {
return await derivePersistedSessionStatus(new SandboxInstancePersistDriver(c.db), command.sessionId);
},
},
run: workflow(runSandboxInstanceWorkflow),
});