mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 20:01:27 +00:00
Complete Foundry refactor checklist
This commit is contained in:
parent
40bed3b0a1
commit
13fc9cb318
91 changed files with 5091 additions and 4108 deletions
|
|
@ -37,6 +37,7 @@ import type {
|
|||
StarSandboxAgentRepoResult,
|
||||
SwitchResult,
|
||||
UpdateFoundryOrganizationProfileInput,
|
||||
WorkspaceModelGroup,
|
||||
WorkspaceModelId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessInfo, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
|
|
@ -73,6 +74,10 @@ export interface ActorConn {
|
|||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
interface AuthSessionScopedInput {
|
||||
authSessionId?: string;
|
||||
}
|
||||
|
||||
interface OrganizationHandle {
|
||||
connect(): ActorConn;
|
||||
listRepos(input: { organizationId: string }): Promise<RepoRecord[]>;
|
||||
|
|
@ -91,24 +96,22 @@ interface OrganizationHandle {
|
|||
useOrganization(input: { organizationId: string }): Promise<{ organizationId: string }>;
|
||||
starSandboxAgentRepo(input: StarSandboxAgentRepoInput): Promise<StarSandboxAgentRepoResult>;
|
||||
getOrganizationSummary(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
adminReconcileWorkspaceState(input: { organizationId: string }): Promise<OrganizationSummarySnapshot>;
|
||||
createWorkspaceTask(input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markWorkspaceUnread(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
renameWorkspaceTask(input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
createWorkspaceSession(input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkspaceDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
changeWorkspaceModel(input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
sendWorkspaceMessage(input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
closeWorkspaceSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
publishWorkspacePr(input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertWorkspaceFile(input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
createWorkspaceTask(input: TaskWorkspaceCreateTaskInput & AuthSessionScopedInput): Promise<TaskWorkspaceCreateTaskResponse>;
|
||||
markWorkspaceUnread(input: TaskWorkspaceSelectInput & AuthSessionScopedInput): Promise<void>;
|
||||
renameWorkspaceTask(input: TaskWorkspaceRenameInput & AuthSessionScopedInput): Promise<void>;
|
||||
createWorkspaceSession(input: TaskWorkspaceSelectInput & { model?: string } & AuthSessionScopedInput): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(input: TaskWorkspaceRenameSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
selectWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(input: TaskWorkspaceSetSessionUnreadInput & AuthSessionScopedInput): Promise<void>;
|
||||
updateWorkspaceDraft(input: TaskWorkspaceUpdateDraftInput & AuthSessionScopedInput): Promise<void>;
|
||||
changeWorkspaceModel(input: TaskWorkspaceChangeModelInput & AuthSessionScopedInput): Promise<void>;
|
||||
sendWorkspaceMessage(input: TaskWorkspaceSendMessageInput & AuthSessionScopedInput): Promise<void>;
|
||||
stopWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
closeWorkspaceSession(input: TaskWorkspaceSessionInput & AuthSessionScopedInput): Promise<void>;
|
||||
publishWorkspacePr(input: TaskWorkspaceSelectInput & AuthSessionScopedInput): Promise<void>;
|
||||
revertWorkspaceFile(input: TaskWorkspaceDiffInput & AuthSessionScopedInput): Promise<void>;
|
||||
adminReloadGithubOrganization(): Promise<void>;
|
||||
adminReloadGithubPullRequests(): Promise<void>;
|
||||
adminReloadGithubRepository(input: { repoId: string }): Promise<void>;
|
||||
adminReloadGithubPullRequest(input: { repoId: string; prNumber: number }): Promise<void>;
|
||||
}
|
||||
|
||||
interface AppOrganizationHandle {
|
||||
|
|
@ -130,8 +133,8 @@ interface AppOrganizationHandle {
|
|||
|
||||
interface TaskHandle {
|
||||
getTaskSummary(): Promise<WorkspaceTaskSummary>;
|
||||
getTaskDetail(): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string }): Promise<WorkspaceSessionDetail>;
|
||||
getTaskDetail(input?: AuthSessionScopedInput): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(input: { sessionId: string } & AuthSessionScopedInput): Promise<WorkspaceSessionDetail>;
|
||||
connect(): ActorConn;
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +159,7 @@ interface TaskSandboxHandle {
|
|||
rawSendSessionMethod(sessionId: string, method: string, params: Record<string, unknown>): Promise<unknown>;
|
||||
destroySession(sessionId: string): Promise<void>;
|
||||
sandboxAgentConnection(): Promise<{ endpoint: string; token?: string }>;
|
||||
listWorkspaceModelGroups(): Promise<WorkspaceModelGroup[]>;
|
||||
providerState(): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
||||
}
|
||||
|
||||
|
|
@ -279,6 +283,7 @@ export interface BackendClient {
|
|||
sandboxId: string,
|
||||
): Promise<{ sandboxProviderId: SandboxProviderId; sandboxId: string; state: string; at: number }>;
|
||||
getSandboxAgentConnection(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<{ endpoint: string; token?: string }>;
|
||||
getSandboxWorkspaceModelGroups(organizationId: string, sandboxProviderId: SandboxProviderId, sandboxId: string): Promise<WorkspaceModelGroup[]>;
|
||||
getOrganizationSummary(organizationId: string): Promise<OrganizationSummarySnapshot>;
|
||||
getTaskDetail(organizationId: string, repoId: string, taskId: string): Promise<WorkspaceTaskDetail>;
|
||||
getSessionDetail(organizationId: string, repoId: string, taskId: string, sessionId: string): Promise<WorkspaceSessionDetail>;
|
||||
|
|
@ -289,6 +294,7 @@ export interface BackendClient {
|
|||
renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void>;
|
||||
createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }>;
|
||||
renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
selectWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void>;
|
||||
|
|
@ -298,9 +304,7 @@ export interface BackendClient {
|
|||
publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void>;
|
||||
revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void>;
|
||||
adminReloadGithubOrganization(organizationId: string): Promise<void>;
|
||||
adminReloadGithubPullRequests(organizationId: string): Promise<void>;
|
||||
adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void>;
|
||||
adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void>;
|
||||
health(): Promise<{ ok: true }>;
|
||||
useOrganization(organizationId: string): Promise<{ organizationId: string }>;
|
||||
starSandboxAgentRepo(organizationId: string): Promise<StarSandboxAgentRepoResult>;
|
||||
|
|
@ -460,6 +464,16 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
||||
};
|
||||
|
||||
const getAuthSessionInput = async (): Promise<AuthSessionScopedInput | undefined> => {
|
||||
const authSessionId = await getSessionId();
|
||||
return authSessionId ? { authSessionId } : undefined;
|
||||
};
|
||||
|
||||
const withAuthSessionInput = async <TInput extends object>(input: TInput): Promise<TInput & AuthSessionScopedInput> => {
|
||||
const authSessionInput = await getAuthSessionInput();
|
||||
return authSessionInput ? { ...input, ...authSessionInput } : input;
|
||||
};
|
||||
|
||||
const organization = async (organizationId: string): Promise<OrganizationHandle> =>
|
||||
client.organization.getOrCreate(organizationKey(organizationId), {
|
||||
createWithInput: organizationId,
|
||||
|
|
@ -492,17 +506,18 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
|
||||
for (const row of candidates) {
|
||||
try {
|
||||
const detail = await ws.getTask({ organizationId, taskId: row.taskId });
|
||||
const detail = await ws.getTask({ organizationId, repoId: row.repoId, taskId: row.taskId });
|
||||
if (detail.sandboxProviderId !== sandboxProviderId) {
|
||||
continue;
|
||||
}
|
||||
const sandbox = detail.sandboxes.find(
|
||||
const sandboxes = detail.sandboxes as Array<(typeof detail.sandboxes)[number] & { sandboxActorId?: string }>;
|
||||
const sandbox = sandboxes.find(
|
||||
(sb) =>
|
||||
sb.sandboxId === sandboxId &&
|
||||
sb.sandboxProviderId === sandboxProviderId &&
|
||||
typeof (sb as any).sandboxActorId === "string" &&
|
||||
(sb as any).sandboxActorId.length > 0,
|
||||
) as { sandboxActorId?: string } | undefined;
|
||||
typeof sb.sandboxActorId === "string" &&
|
||||
sb.sandboxActorId.length > 0,
|
||||
);
|
||||
if (sandbox?.sandboxActorId) {
|
||||
return (client as any).taskSandbox.getForId(sandbox.sandboxActorId);
|
||||
}
|
||||
|
|
@ -562,14 +577,28 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
}
|
||||
};
|
||||
|
||||
const getTaskDetailWithAuth = async (organizationId: string, repoId: string, taskIdValue: string): Promise<WorkspaceTaskDetail> => {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getTaskDetail(await getAuthSessionInput());
|
||||
};
|
||||
|
||||
const getSessionDetailWithAuth = async (
|
||||
organizationId: string,
|
||||
repoId: string,
|
||||
taskIdValue: string,
|
||||
sessionId: string,
|
||||
): Promise<WorkspaceSessionDetail> => {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getSessionDetail(await withAuthSessionInput({ sessionId }));
|
||||
};
|
||||
|
||||
const getWorkspaceCompat = async (organizationId: string): Promise<TaskWorkspaceSnapshot> => {
|
||||
const authSessionInput = await getAuthSessionInput();
|
||||
const summary = await (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
const tasks = (
|
||||
await Promise.all(
|
||||
summary.taskSummaries.map(async (taskSummary) => {
|
||||
const resolvedTasks = await Promise.all(
|
||||
summary.taskSummaries.map(async (taskSummary) => {
|
||||
let detail;
|
||||
try {
|
||||
detail = await (await task(organizationId, taskSummary.repoId, taskSummary.id)).getTaskDetail();
|
||||
const taskHandle = await task(organizationId, taskSummary.repoId, taskSummary.id);
|
||||
detail = await taskHandle.getTaskDetail(authSessionInput);
|
||||
} catch (error) {
|
||||
if (isActorNotFoundError(error)) {
|
||||
return null;
|
||||
|
|
@ -579,7 +608,10 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
const sessionDetails = await Promise.all(
|
||||
detail.sessionsSummary.map(async (session) => {
|
||||
try {
|
||||
const full = await (await task(organizationId, detail.repoId, detail.id)).getSessionDetail({ sessionId: session.id });
|
||||
const full = await (await task(organizationId, detail.repoId, detail.id)).getSessionDetail({
|
||||
sessionId: session.id,
|
||||
...(authSessionInput ?? {}),
|
||||
});
|
||||
return [session.id, full] as const;
|
||||
} catch (error) {
|
||||
if (isActorNotFoundError(error)) {
|
||||
|
|
@ -599,6 +631,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
updatedAtMs: detail.updatedAtMs,
|
||||
branch: detail.branch,
|
||||
pullRequest: detail.pullRequest,
|
||||
activeSessionId: detail.activeSessionId ?? null,
|
||||
sessions: detail.sessionsSummary.map((session) => {
|
||||
const full = sessionDetailsById.get(session.id);
|
||||
return {
|
||||
|
|
@ -619,10 +652,11 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
diffs: detail.diffs,
|
||||
fileTree: detail.fileTree,
|
||||
minutesUsed: detail.minutesUsed,
|
||||
activeSandboxId: detail.activeSandboxId ?? null,
|
||||
};
|
||||
}),
|
||||
)
|
||||
).filter((task): task is TaskWorkspaceSnapshot["tasks"][number] => task !== null);
|
||||
);
|
||||
const tasks = resolvedTasks.filter((task): task is Exclude<(typeof resolvedTasks)[number], null> => task !== null);
|
||||
|
||||
const repositories = summary.repos
|
||||
.map((repo) => ({
|
||||
|
|
@ -1170,16 +1204,24 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return await withSandboxHandle(organizationId, sandboxProviderId, sandboxId, async (handle) => handle.sandboxAgentConnection());
|
||||
},
|
||||
|
||||
async getSandboxWorkspaceModelGroups(
|
||||
organizationId: string,
|
||||
sandboxProviderId: SandboxProviderId,
|
||||
sandboxId: string,
|
||||
): Promise<WorkspaceModelGroup[]> {
|
||||
return await withSandboxHandle(organizationId, sandboxProviderId, sandboxId, async (handle) => handle.listWorkspaceModelGroups());
|
||||
},
|
||||
|
||||
async getOrganizationSummary(organizationId: string): Promise<OrganizationSummarySnapshot> {
|
||||
return (await organization(organizationId)).getOrganizationSummary({ organizationId });
|
||||
},
|
||||
|
||||
async getTaskDetail(organizationId: string, repoId: string, taskIdValue: string): Promise<WorkspaceTaskDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getTaskDetail();
|
||||
return await getTaskDetailWithAuth(organizationId, repoId, taskIdValue);
|
||||
},
|
||||
|
||||
async getSessionDetail(organizationId: string, repoId: string, taskIdValue: string, sessionId: string): Promise<WorkspaceSessionDetail> {
|
||||
return (await task(organizationId, repoId, taskIdValue)).getSessionDetail({ sessionId });
|
||||
return await getSessionDetailWithAuth(organizationId, repoId, taskIdValue, sessionId);
|
||||
},
|
||||
|
||||
async getWorkspace(organizationId: string): Promise<TaskWorkspaceSnapshot> {
|
||||
|
|
@ -1191,73 +1233,69 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
},
|
||||
|
||||
async createWorkspaceTask(organizationId: string, input: TaskWorkspaceCreateTaskInput): Promise<TaskWorkspaceCreateTaskResponse> {
|
||||
return (await organization(organizationId)).createWorkspaceTask(input);
|
||||
return (await organization(organizationId)).createWorkspaceTask(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async markWorkspaceUnread(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).markWorkspaceUnread(input);
|
||||
await (await organization(organizationId)).markWorkspaceUnread(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async renameWorkspaceTask(organizationId: string, input: TaskWorkspaceRenameInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkspaceTask(input);
|
||||
await (await organization(organizationId)).renameWorkspaceTask(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async createWorkspaceSession(organizationId: string, input: TaskWorkspaceSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
return await (await organization(organizationId)).createWorkspaceSession(input);
|
||||
return await (await organization(organizationId)).createWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async renameWorkspaceSession(organizationId: string, input: TaskWorkspaceRenameSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).renameWorkspaceSession(input);
|
||||
await (await organization(organizationId)).renameWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async selectWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).selectWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async setWorkspaceSessionUnread(organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await (await organization(organizationId)).setWorkspaceSessionUnread(input);
|
||||
await (await organization(organizationId)).setWorkspaceSessionUnread(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async updateWorkspaceDraft(organizationId: string, input: TaskWorkspaceUpdateDraftInput): Promise<void> {
|
||||
await (await organization(organizationId)).updateWorkspaceDraft(input);
|
||||
await (await organization(organizationId)).updateWorkspaceDraft(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async changeWorkspaceModel(organizationId: string, input: TaskWorkspaceChangeModelInput): Promise<void> {
|
||||
await (await organization(organizationId)).changeWorkspaceModel(input);
|
||||
await (await organization(organizationId)).changeWorkspaceModel(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async sendWorkspaceMessage(organizationId: string, input: TaskWorkspaceSendMessageInput): Promise<void> {
|
||||
await (await organization(organizationId)).sendWorkspaceMessage(input);
|
||||
await (await organization(organizationId)).sendWorkspaceMessage(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async stopWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).stopWorkspaceSession(input);
|
||||
await (await organization(organizationId)).stopWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async closeWorkspaceSession(organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await (await organization(organizationId)).closeWorkspaceSession(input);
|
||||
await (await organization(organizationId)).closeWorkspaceSession(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async publishWorkspacePr(organizationId: string, input: TaskWorkspaceSelectInput): Promise<void> {
|
||||
await (await organization(organizationId)).publishWorkspacePr(input);
|
||||
await (await organization(organizationId)).publishWorkspacePr(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async revertWorkspaceFile(organizationId: string, input: TaskWorkspaceDiffInput): Promise<void> {
|
||||
await (await organization(organizationId)).revertWorkspaceFile(input);
|
||||
await (await organization(organizationId)).revertWorkspaceFile(await withAuthSessionInput(input));
|
||||
},
|
||||
|
||||
async adminReloadGithubOrganization(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubOrganization();
|
||||
},
|
||||
|
||||
async adminReloadGithubPullRequests(organizationId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubPullRequests();
|
||||
},
|
||||
|
||||
async adminReloadGithubRepository(organizationId: string, repoId: string): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubRepository({ repoId });
|
||||
},
|
||||
|
||||
async adminReloadGithubPullRequest(organizationId: string, repoId: string, prNumber: number): Promise<void> {
|
||||
await (await organization(organizationId)).adminReloadGithubPullRequest({ repoId, prNumber });
|
||||
},
|
||||
|
||||
async health(): Promise<{ ok: true }> {
|
||||
const organizationId = options.defaultOrganizationId;
|
||||
if (!organizationId) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import type { WorkspaceModelId } from "@sandbox-agent/foundry-shared";
|
||||
import { DEFAULT_WORKSPACE_MODEL_GROUPS, DEFAULT_WORKSPACE_MODEL_ID, type WorkspaceModelId } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
const claudeModels = DEFAULT_WORKSPACE_MODEL_GROUPS.find((group) => group.agentKind === "Claude")?.models ?? [];
|
||||
const CLAUDE_SECONDARY_MODEL_ID = claudeModels[1]?.id ?? claudeModels[0]?.id ?? DEFAULT_WORKSPACE_MODEL_ID;
|
||||
const CLAUDE_TERTIARY_MODEL_ID = claudeModels[2]?.id ?? CLAUDE_SECONDARY_MODEL_ID;
|
||||
import { injectMockLatency } from "./mock/latency.js";
|
||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||
|
||||
|
|
@ -233,7 +237,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "nathan",
|
||||
roleLabel: "Founder",
|
||||
eligibleOrganizationIds: ["personal-nathan", "acme", "rivet"],
|
||||
defaultModel: "gpt-5.3-codex",
|
||||
defaultModel: DEFAULT_WORKSPACE_MODEL_ID,
|
||||
},
|
||||
{
|
||||
id: "user-maya",
|
||||
|
|
@ -242,7 +246,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "maya",
|
||||
roleLabel: "Staff Engineer",
|
||||
eligibleOrganizationIds: ["acme"],
|
||||
defaultModel: "claude-sonnet-4",
|
||||
defaultModel: CLAUDE_SECONDARY_MODEL_ID,
|
||||
},
|
||||
{
|
||||
id: "user-jamie",
|
||||
|
|
@ -251,7 +255,7 @@ function buildDefaultSnapshot(): MockFoundryAppSnapshot {
|
|||
githubLogin: "jamie",
|
||||
roleLabel: "Platform Lead",
|
||||
eligibleOrganizationIds: ["personal-jamie", "rivet"],
|
||||
defaultModel: "claude-opus-4",
|
||||
defaultModel: CLAUDE_TERTIARY_MODEL_ID,
|
||||
},
|
||||
],
|
||||
organizations: [
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type {
|
|||
TaskWorkspaceUpdateDraftInput,
|
||||
TaskEvent,
|
||||
WorkspaceSessionDetail,
|
||||
WorkspaceModelGroup,
|
||||
WorkspaceTaskDetail,
|
||||
WorkspaceTaskSummary,
|
||||
OrganizationEvent,
|
||||
|
|
@ -32,6 +33,7 @@ import type {
|
|||
StarSandboxAgentRepoResult,
|
||||
SwitchResult,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { DEFAULT_WORKSPACE_MODEL_GROUPS } from "@sandbox-agent/foundry-shared";
|
||||
import type { ProcessCreateRequest, ProcessLogFollowQuery, ProcessLogsResponse, ProcessSignalQuery } from "sandbox-agent";
|
||||
import type { ActorConn, BackendClient, SandboxProcessRecord, SandboxSessionEventRecord, SandboxSessionRecord } from "../backend-client.js";
|
||||
import { getSharedMockWorkspaceClient } from "./workspace-client.js";
|
||||
|
|
@ -173,6 +175,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
updatedAtMs: task.updatedAtMs,
|
||||
branch: task.branch,
|
||||
pullRequest: task.pullRequest,
|
||||
activeSessionId: task.activeSessionId ?? task.sessions[0]?.id ?? null,
|
||||
sessionsSummary: task.sessions.map((tab) => ({
|
||||
id: tab.id,
|
||||
sessionId: tab.sessionId,
|
||||
|
|
@ -190,13 +193,6 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
const buildTaskDetail = (task: TaskWorkspaceSnapshot["tasks"][number]): WorkspaceTaskDetail => ({
|
||||
...buildTaskSummary(task),
|
||||
task: task.title,
|
||||
agentType: task.sessions[0]?.agent === "Codex" ? "codex" : "claude",
|
||||
runtimeStatus: toTaskStatus(task.status === "archived" ? "archived" : "running", task.status === "archived"),
|
||||
statusMessage: task.status === "archived" ? "archived" : "mock sandbox ready",
|
||||
activeSessionId: task.sessions[0]?.sessionId ?? null,
|
||||
diffStat: task.fileChanges.length > 0 ? `+${task.fileChanges.length}/-${task.fileChanges.length}` : "+0/-0",
|
||||
prUrl: task.pullRequest ? `https://example.test/pr/${task.pullRequest.number}` : null,
|
||||
reviewStatus: null,
|
||||
fileChanges: task.fileChanges,
|
||||
diffs: task.diffs,
|
||||
fileTree: task.fileTree,
|
||||
|
|
@ -236,6 +232,20 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
const taskSummaries = snapshot.tasks.map(buildTaskSummary);
|
||||
return {
|
||||
organizationId: defaultOrganizationId,
|
||||
github: {
|
||||
connectedAccount: "mock",
|
||||
installationStatus: "connected",
|
||||
syncStatus: "synced",
|
||||
importedRepoCount: snapshot.repos.length,
|
||||
lastSyncLabel: "Synced just now",
|
||||
lastSyncAt: nowMs(),
|
||||
lastWebhookAt: null,
|
||||
lastWebhookEvent: "",
|
||||
syncGeneration: 1,
|
||||
syncPhase: null,
|
||||
processedRepositoryCount: snapshot.repos.length,
|
||||
totalRepositoryCount: snapshot.repos.length,
|
||||
},
|
||||
repos: snapshot.repos.map((repo) => {
|
||||
const repoTasks = taskSummaries.filter((task) => task.repoId === repo.id);
|
||||
return {
|
||||
|
|
@ -298,9 +308,7 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
task: task.title,
|
||||
sandboxProviderId: "local",
|
||||
status: toTaskStatus(archived ? "archived" : "running", archived),
|
||||
statusMessage: archived ? "archived" : "mock sandbox ready",
|
||||
activeSandboxId: task.id,
|
||||
activeSessionId: task.sessions[0]?.sessionId ?? null,
|
||||
sandboxes: [
|
||||
{
|
||||
sandboxId: task.id,
|
||||
|
|
@ -312,17 +320,6 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
updatedAt: task.updatedAtMs,
|
||||
},
|
||||
],
|
||||
agentType: task.sessions[0]?.agent === "Codex" ? "codex" : "claude",
|
||||
prSubmitted: Boolean(task.pullRequest),
|
||||
diffStat: task.fileChanges.length > 0 ? `+${task.fileChanges.length}/-${task.fileChanges.length}` : "+0/-0",
|
||||
prUrl: task.pullRequest ? `https://example.test/pr/${task.pullRequest.number}` : null,
|
||||
prAuthor: task.pullRequest ? "mock" : null,
|
||||
ciStatus: null,
|
||||
reviewStatus: null,
|
||||
reviewer: null,
|
||||
conflictsWithMain: "0",
|
||||
hasUnpushed: task.fileChanges.length > 0 ? "1" : "0",
|
||||
parentBranch: null,
|
||||
createdAt: task.updatedAtMs,
|
||||
updatedAt: task.updatedAtMs,
|
||||
};
|
||||
|
|
@ -636,6 +633,14 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
return { endpoint: "mock://terminal-unavailable" };
|
||||
},
|
||||
|
||||
async getSandboxWorkspaceModelGroups(
|
||||
_organizationId: string,
|
||||
_sandboxProviderId: SandboxProviderId,
|
||||
_sandboxId: string,
|
||||
): Promise<WorkspaceModelGroup[]> {
|
||||
return DEFAULT_WORKSPACE_MODEL_GROUPS;
|
||||
},
|
||||
|
||||
async getOrganizationSummary(): Promise<OrganizationSummarySnapshot> {
|
||||
return buildOrganizationSummary();
|
||||
},
|
||||
|
|
@ -693,6 +698,13 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async selectWorkspaceSession(_organizationId: string, input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await workspace.selectSession(input);
|
||||
emitOrganizationSnapshot();
|
||||
emitTaskUpdate(input.taskId);
|
||||
emitSessionUpdate(input.taskId, input.sessionId);
|
||||
},
|
||||
|
||||
async setWorkspaceSessionUnread(_organizationId: string, input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await workspace.setSessionUnread(input);
|
||||
emitOrganizationSnapshot();
|
||||
|
|
@ -747,13 +759,8 @@ export function createMockBackendClient(defaultOrganizationId = "default"): Back
|
|||
},
|
||||
|
||||
async adminReloadGithubOrganization(): Promise<void> {},
|
||||
|
||||
async adminReloadGithubPullRequests(): Promise<void> {},
|
||||
|
||||
async adminReloadGithubRepository(): Promise<void> {},
|
||||
|
||||
async adminReloadGithubPullRequest(): Promise<void> {},
|
||||
|
||||
async health(): Promise<{ ok: true }> {
|
||||
return { ok: true };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
slugify,
|
||||
uid,
|
||||
} from "../workspace-model.js";
|
||||
import { DEFAULT_WORKSPACE_MODEL_ID, workspaceAgentForModel } from "@sandbox-agent/foundry-shared";
|
||||
import type {
|
||||
TaskWorkspaceAddSessionResponse,
|
||||
TaskWorkspaceChangeModelInput,
|
||||
|
|
@ -74,20 +75,19 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
id,
|
||||
repoId: repo.id,
|
||||
title: input.title?.trim() || "New Task",
|
||||
status: "new",
|
||||
status: "init_enqueue_provision",
|
||||
repoName: repo.label,
|
||||
updatedAtMs: nowMs(),
|
||||
branch: input.branch?.trim() || null,
|
||||
pullRequest: null,
|
||||
activeSessionId: sessionId,
|
||||
sessions: [
|
||||
{
|
||||
id: sessionId,
|
||||
sessionId: sessionId,
|
||||
sessionName: "Session 1",
|
||||
agent: providerAgent(
|
||||
MODEL_GROUPS.find((group) => group.models.some((model) => model.id === (input.model ?? "claude-sonnet-4")))?.provider ?? "Claude",
|
||||
),
|
||||
model: input.model ?? "claude-sonnet-4",
|
||||
agent: workspaceAgentForModel(input.model ?? DEFAULT_WORKSPACE_MODEL_ID, MODEL_GROUPS),
|
||||
model: input.model ?? DEFAULT_WORKSPACE_MODEL_ID,
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -140,7 +140,18 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
this.updateTask(input.taskId, (task) => ({
|
||||
...task,
|
||||
updatedAtMs: nowMs(),
|
||||
pullRequest: { number: nextPrNumber, status: "ready" },
|
||||
pullRequest: {
|
||||
number: nextPrNumber,
|
||||
title: task.title,
|
||||
state: "open",
|
||||
url: `https://example.test/pr/${nextPrNumber}`,
|
||||
headRefName: task.branch ?? `task/${task.id}`,
|
||||
baseRefName: "main",
|
||||
repoFullName: task.repoName,
|
||||
authorLogin: "mock",
|
||||
isDraft: false,
|
||||
updatedAtMs: nowMs(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +200,7 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
const startedAtMs = nowMs();
|
||||
|
||||
this.updateTask(input.taskId, (currentTask) => {
|
||||
const isFirstOnTask = currentTask.status === "new";
|
||||
const isFirstOnTask = String(currentTask.status).startsWith("init_");
|
||||
const newTitle = isFirstOnTask ? (text.length > 50 ? `${text.slice(0, 47)}...` : text) : currentTask.title;
|
||||
const newBranch = isFirstOnTask ? `feat/${slugify(newTitle)}` : currentTask.branch;
|
||||
const userMessageLines = [text, ...input.attachments.map((attachment) => `@ ${attachment.filePath}:${attachment.lineNumber}`)];
|
||||
|
|
@ -303,6 +314,14 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
});
|
||||
}
|
||||
|
||||
async selectSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
this.assertSession(input.taskId, input.sessionId);
|
||||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
activeSessionId: input.sessionId,
|
||||
}));
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
|
|
@ -329,6 +348,7 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
|
||||
return {
|
||||
...currentTask,
|
||||
activeSessionId: currentTask.activeSessionId === input.sessionId ? (currentTask.sessions.find((candidate) => candidate.id !== input.sessionId)?.id ?? null) : currentTask.activeSessionId,
|
||||
sessions: currentTask.sessions.filter((candidate) => candidate.id !== input.sessionId),
|
||||
};
|
||||
});
|
||||
|
|
@ -342,8 +362,8 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
sessionId: nextSessionId,
|
||||
sandboxSessionId: null,
|
||||
sessionName: `Session ${this.requireTask(input.taskId).sessions.length + 1}`,
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
agent: workspaceAgentForModel(DEFAULT_WORKSPACE_MODEL_ID, MODEL_GROUPS),
|
||||
model: DEFAULT_WORKSPACE_MODEL_ID,
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -355,6 +375,7 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
updatedAtMs: nowMs(),
|
||||
activeSessionId: nextSession.id,
|
||||
sessions: [...currentTask.sessions, nextSession],
|
||||
}));
|
||||
return { sessionId: nextSession.id };
|
||||
|
|
@ -369,7 +390,7 @@ class MockWorkspaceStore implements TaskWorkspaceClient {
|
|||
this.updateTask(input.taskId, (currentTask) => ({
|
||||
...currentTask,
|
||||
sessions: currentTask.sessions.map((candidate) =>
|
||||
candidate.id === input.sessionId ? { ...candidate, model: input.model, agent: providerAgent(group.provider) } : candidate,
|
||||
candidate.id === input.sessionId ? { ...candidate, model: input.model, agent: workspaceAgentForModel(input.model, MODEL_GROUPS) } : candidate,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,11 @@ class RemoteWorkspaceStore implements TaskWorkspaceClient {
|
|||
await this.refresh();
|
||||
}
|
||||
|
||||
async selectSession(input: TaskWorkspaceSessionInput): Promise<void> {
|
||||
await this.backend.selectWorkspaceSession(this.organizationId, input);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void> {
|
||||
await this.backend.setWorkspaceSessionUnread(this.organizationId, input);
|
||||
await this.refresh();
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class TopicEntry<TData, TParams, TEvent> {
|
|||
private unsubscribeError: (() => void) | null = null;
|
||||
private teardownTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private startPromise: Promise<void> | null = null;
|
||||
private eventPromise: Promise<void> = Promise.resolve();
|
||||
private started = false;
|
||||
|
||||
constructor(
|
||||
|
|
@ -157,12 +158,7 @@ class TopicEntry<TData, TParams, TEvent> {
|
|||
try {
|
||||
this.conn = await this.definition.connect(this.backend, this.params);
|
||||
this.unsubscribeEvent = this.conn.on(this.definition.event, (event: TEvent) => {
|
||||
if (this.data === undefined) {
|
||||
return;
|
||||
}
|
||||
this.data = this.definition.applyEvent(this.data, event);
|
||||
this.lastRefreshAt = Date.now();
|
||||
this.notify();
|
||||
void this.applyEvent(event);
|
||||
});
|
||||
this.unsubscribeError = this.conn.onError((error: unknown) => {
|
||||
this.status = "error";
|
||||
|
|
@ -182,6 +178,33 @@ class TopicEntry<TData, TParams, TEvent> {
|
|||
}
|
||||
}
|
||||
|
||||
private applyEvent(event: TEvent): Promise<void> {
|
||||
this.eventPromise = this.eventPromise
|
||||
.then(async () => {
|
||||
if (!this.started || this.data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextData = await this.definition.applyEvent(this.backend, this.params, this.data, event);
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = nextData;
|
||||
this.status = "connected";
|
||||
this.error = null;
|
||||
this.lastRefreshAt = Date.now();
|
||||
this.notify();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.status = "error";
|
||||
this.error = error instanceof Error ? error : new Error(String(error));
|
||||
this.notify();
|
||||
});
|
||||
|
||||
return this.eventPromise;
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
for (const listener of [...this.listeners]) {
|
||||
listener();
|
||||
|
|
|
|||
|
|
@ -16,15 +16,15 @@ import type { ActorConn, BackendClient, SandboxProcessRecord } from "../backend-
|
|||
* Topic definitions for the subscription manager.
|
||||
*
|
||||
* Each topic describes one actor connection plus one materialized read model.
|
||||
* Events always carry full replacement payloads for the changed entity so the
|
||||
* client can replace cached state directly instead of reconstructing patches.
|
||||
* Some topics can apply broadcast payloads directly, while others refetch
|
||||
* through BackendClient so auth-scoped state stays user-specific.
|
||||
*/
|
||||
export interface TopicDefinition<TData, TParams, TEvent> {
|
||||
key: (params: TParams) => string;
|
||||
event: string;
|
||||
connect: (backend: BackendClient, params: TParams) => Promise<ActorConn>;
|
||||
fetchInitial: (backend: BackendClient, params: TParams) => Promise<TData>;
|
||||
applyEvent: (current: TData, event: TEvent) => TData;
|
||||
applyEvent: (backend: BackendClient, params: TParams, current: TData, event: TEvent) => Promise<TData> | TData;
|
||||
}
|
||||
|
||||
export interface AppTopicParams {}
|
||||
|
|
@ -54,7 +54,7 @@ export const topicDefinitions = {
|
|||
event: "appUpdated",
|
||||
connect: (backend: BackendClient, _params: AppTopicParams) => backend.connectOrganization("app"),
|
||||
fetchInitial: (backend: BackendClient, _params: AppTopicParams) => backend.getAppSnapshot(),
|
||||
applyEvent: (_current: FoundryAppSnapshot, event: AppEvent) => event.snapshot,
|
||||
applyEvent: (_backend: BackendClient, _params: AppTopicParams, _current: FoundryAppSnapshot, event: AppEvent) => event.snapshot,
|
||||
} satisfies TopicDefinition<FoundryAppSnapshot, AppTopicParams, AppEvent>,
|
||||
|
||||
organization: {
|
||||
|
|
@ -62,7 +62,8 @@ export const topicDefinitions = {
|
|||
event: "organizationUpdated",
|
||||
connect: (backend: BackendClient, params: OrganizationTopicParams) => backend.connectOrganization(params.organizationId),
|
||||
fetchInitial: (backend: BackendClient, params: OrganizationTopicParams) => backend.getOrganizationSummary(params.organizationId),
|
||||
applyEvent: (_current: OrganizationSummarySnapshot, event: OrganizationEvent) => event.snapshot,
|
||||
applyEvent: (_backend: BackendClient, _params: OrganizationTopicParams, _current: OrganizationSummarySnapshot, event: OrganizationEvent) =>
|
||||
event.snapshot,
|
||||
} satisfies TopicDefinition<OrganizationSummarySnapshot, OrganizationTopicParams, OrganizationEvent>,
|
||||
|
||||
task: {
|
||||
|
|
@ -70,7 +71,8 @@ export const topicDefinitions = {
|
|||
event: "taskUpdated",
|
||||
connect: (backend: BackendClient, params: TaskTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
||||
fetchInitial: (backend: BackendClient, params: TaskTopicParams) => backend.getTaskDetail(params.organizationId, params.repoId, params.taskId),
|
||||
applyEvent: (_current: WorkspaceTaskDetail, event: TaskEvent) => event.detail,
|
||||
applyEvent: (backend: BackendClient, params: TaskTopicParams, _current: WorkspaceTaskDetail, _event: TaskEvent) =>
|
||||
backend.getTaskDetail(params.organizationId, params.repoId, params.taskId),
|
||||
} satisfies TopicDefinition<WorkspaceTaskDetail, TaskTopicParams, TaskEvent>,
|
||||
|
||||
session: {
|
||||
|
|
@ -79,11 +81,11 @@ export const topicDefinitions = {
|
|||
connect: (backend: BackendClient, params: SessionTopicParams) => backend.connectTask(params.organizationId, params.repoId, params.taskId),
|
||||
fetchInitial: (backend: BackendClient, params: SessionTopicParams) =>
|
||||
backend.getSessionDetail(params.organizationId, params.repoId, params.taskId, params.sessionId),
|
||||
applyEvent: (current: WorkspaceSessionDetail, event: SessionEvent) => {
|
||||
if (event.session.sessionId !== current.sessionId) {
|
||||
applyEvent: async (backend: BackendClient, params: SessionTopicParams, current: WorkspaceSessionDetail, event: SessionEvent) => {
|
||||
if (event.session.sessionId !== params.sessionId) {
|
||||
return current;
|
||||
}
|
||||
return event.session;
|
||||
return await backend.getSessionDetail(params.organizationId, params.repoId, params.taskId, params.sessionId);
|
||||
},
|
||||
} satisfies TopicDefinition<WorkspaceSessionDetail, SessionTopicParams, SessionEvent>,
|
||||
|
||||
|
|
@ -94,7 +96,8 @@ export const topicDefinitions = {
|
|||
backend.connectSandbox(params.organizationId, params.sandboxProviderId, params.sandboxId),
|
||||
fetchInitial: async (backend: BackendClient, params: SandboxProcessesTopicParams) =>
|
||||
(await backend.listSandboxProcesses(params.organizationId, params.sandboxProviderId, params.sandboxId)).processes,
|
||||
applyEvent: (_current: SandboxProcessRecord[], event: SandboxProcessesEvent) => event.processes,
|
||||
applyEvent: (_backend: BackendClient, _params: SandboxProcessesTopicParams, _current: SandboxProcessRecord[], event: SandboxProcessesEvent) =>
|
||||
event.processes,
|
||||
} satisfies TopicDefinition<SandboxProcessRecord[], SandboxProcessesTopicParams, SandboxProcessesEvent>,
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function filterTasks(rows: TaskRecord[], query: string): TaskRecord[] {
|
|||
}
|
||||
|
||||
return rows.filter((row) => {
|
||||
const fields = [row.branchName ?? "", row.title ?? "", row.taskId, row.task, row.prAuthor ?? "", row.reviewer ?? ""];
|
||||
const fields = [row.branchName ?? "", row.title ?? "", row.taskId, row.task];
|
||||
return fields.some((field) => fuzzyMatch(field, q));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export interface TaskWorkspaceClient {
|
|||
updateDraft(input: TaskWorkspaceUpdateDraftInput): Promise<void>;
|
||||
sendMessage(input: TaskWorkspaceSendMessageInput): Promise<void>;
|
||||
stopAgent(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
selectSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
setSessionUnread(input: TaskWorkspaceSetSessionUnreadInput): Promise<void>;
|
||||
renameSession(input: TaskWorkspaceRenameSessionInput): Promise<void>;
|
||||
closeSession(input: TaskWorkspaceSessionInput): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
import {
|
||||
DEFAULT_WORKSPACE_MODEL_ID,
|
||||
DEFAULT_WORKSPACE_MODEL_GROUPS as SharedModelGroups,
|
||||
workspaceModelLabel as sharedWorkspaceModelLabel,
|
||||
workspaceProviderAgent as sharedWorkspaceProviderAgent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import type {
|
||||
WorkspaceAgentKind as AgentKind,
|
||||
WorkspaceSession as AgentSession,
|
||||
|
|
@ -15,26 +21,8 @@ import type {
|
|||
} from "@sandbox-agent/foundry-shared";
|
||||
import rivetDevFixture from "../../../scripts/data/rivet-dev.json" with { type: "json" };
|
||||
|
||||
export const MODEL_GROUPS: ModelGroup[] = [
|
||||
{
|
||||
provider: "Claude",
|
||||
models: [
|
||||
{ id: "claude-sonnet-4", label: "Sonnet 4" },
|
||||
{ id: "claude-opus-4", label: "Opus 4" },
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: "OpenAI",
|
||||
models: [
|
||||
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4", label: "GPT-5.4" },
|
||||
{ id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.2", label: "GPT-5.2" },
|
||||
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
|
||||
],
|
||||
},
|
||||
];
|
||||
export const MODEL_GROUPS: ModelGroup[] = SharedModelGroups;
|
||||
export const DEFAULT_MODEL_ID: ModelId = DEFAULT_WORKSPACE_MODEL_ID;
|
||||
|
||||
const MOCK_REPLIES = [
|
||||
"Got it. I'll work on that now. Let me start by examining the relevant files...",
|
||||
|
|
@ -73,15 +61,11 @@ export function formatMessageDuration(durationMs: number): string {
|
|||
}
|
||||
|
||||
export function modelLabel(id: ModelId): string {
|
||||
const group = MODEL_GROUPS.find((candidate) => candidate.models.some((model) => model.id === id));
|
||||
const model = group?.models.find((candidate) => candidate.id === id);
|
||||
return model && group ? `${group.provider} ${model.label}` : id;
|
||||
return sharedWorkspaceModelLabel(id, MODEL_GROUPS);
|
||||
}
|
||||
|
||||
export function providerAgent(provider: string): AgentKind {
|
||||
if (provider === "Claude") return "Claude";
|
||||
if (provider === "OpenAI") return "Codex";
|
||||
return "Cursor";
|
||||
return sharedWorkspaceProviderAgent(provider);
|
||||
}
|
||||
|
||||
export function slugify(text: string): string {
|
||||
|
|
@ -204,6 +188,28 @@ export function buildHistoryEvents(sessions: AgentSession[]): HistoryEvent[] {
|
|||
.sort((left, right) => messageOrder(left.messageId) - messageOrder(right.messageId));
|
||||
}
|
||||
|
||||
function buildPullRequestSummary(params: {
|
||||
number: number;
|
||||
title: string;
|
||||
branch: string;
|
||||
repoName: string;
|
||||
updatedAtMs: number;
|
||||
status: "ready" | "draft";
|
||||
}) {
|
||||
return {
|
||||
number: params.number,
|
||||
title: params.title,
|
||||
state: "open",
|
||||
url: `https://github.com/${params.repoName}/pull/${params.number}`,
|
||||
headRefName: params.branch,
|
||||
baseRefName: "main",
|
||||
repoFullName: params.repoName,
|
||||
authorLogin: "mock",
|
||||
isDraft: params.status === "draft",
|
||||
updatedAtMs: params.updatedAtMs,
|
||||
};
|
||||
}
|
||||
|
||||
function transcriptFromLegacyMessages(sessionId: string, messages: LegacyMessage[]): TranscriptEvent[] {
|
||||
return messages.map((message, index) => ({
|
||||
id: message.id,
|
||||
|
|
@ -315,14 +321,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(8),
|
||||
branch: "NathanFlurry/pi-bootstrap-fix",
|
||||
pullRequest: { number: 227, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 227,
|
||||
title: "Normalize Pi ACP bootstrap payloads",
|
||||
branch: "NathanFlurry/pi-bootstrap-fix",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(8),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t1",
|
||||
sessionId: "t1",
|
||||
sessionName: "Pi payload fix",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -484,14 +497,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(3),
|
||||
branch: "feat/builtin-agent-skills",
|
||||
pullRequest: { number: 223, status: "draft" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 223,
|
||||
title: "Auto-inject builtin agent skills at startup",
|
||||
branch: "feat/builtin-agent-skills",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(3),
|
||||
status: "draft",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t3",
|
||||
sessionId: "t3",
|
||||
sessionName: "Skills injection",
|
||||
agent: "Claude",
|
||||
model: "claude-opus-4",
|
||||
model: "opus",
|
||||
status: "running",
|
||||
thinkingSinceMs: NOW_MS - 45_000,
|
||||
unread: false,
|
||||
|
|
@ -584,14 +604,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(45),
|
||||
branch: "hooks-example",
|
||||
pullRequest: { number: 225, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 225,
|
||||
title: "Add hooks example for Claude, Codex, and OpenCode",
|
||||
branch: "hooks-example",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(45),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t4",
|
||||
sessionId: "t4",
|
||||
sessionName: "Example docs",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -659,14 +686,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(15),
|
||||
branch: "actor-reschedule-endpoint",
|
||||
pullRequest: { number: 4400, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 4400,
|
||||
title: "Add actor reschedule endpoint",
|
||||
branch: "actor-reschedule-endpoint",
|
||||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(15),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t5",
|
||||
sessionId: "t5",
|
||||
sessionName: "Reschedule API",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -793,14 +827,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(35),
|
||||
branch: "feat/dynamic-actors",
|
||||
pullRequest: { number: 4395, status: "draft" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 4395,
|
||||
title: "Dynamic actors",
|
||||
branch: "feat/dynamic-actors",
|
||||
repoName: "rivet-dev/rivet",
|
||||
updatedAtMs: minutesAgo(35),
|
||||
status: "draft",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t6",
|
||||
sessionId: "t6",
|
||||
sessionName: "Dynamic actors impl",
|
||||
agent: "Claude",
|
||||
model: "claude-opus-4",
|
||||
model: "opus",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: true,
|
||||
|
|
@ -850,14 +891,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/vbare",
|
||||
updatedAtMs: minutesAgo(25),
|
||||
branch: "fix-use-full-cloud-run-pool-name",
|
||||
pullRequest: { number: 235, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 235,
|
||||
title: "Use full cloud run pool name for routing",
|
||||
branch: "fix-use-full-cloud-run-pool-name",
|
||||
repoName: "rivet-dev/vbare",
|
||||
updatedAtMs: minutesAgo(25),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t7",
|
||||
sessionId: "t7",
|
||||
sessionName: "Pool routing fix",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -959,14 +1007,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(50),
|
||||
branch: "fix-guard-support-https-targets",
|
||||
pullRequest: { number: 125, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 125,
|
||||
title: "Route compute gateway path correctly",
|
||||
branch: "fix-guard-support-https-targets",
|
||||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(50),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t8",
|
||||
sessionId: "t8",
|
||||
sessionName: "Guard routing",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1073,14 +1128,21 @@ export function buildInitialTasks(): Task[] {
|
|||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(2 * 24 * 60),
|
||||
branch: "chore-move-compute-gateway-to",
|
||||
pullRequest: { number: 123, status: "ready" },
|
||||
pullRequest: buildPullRequestSummary({
|
||||
number: 123,
|
||||
title: "Move compute gateway to guard",
|
||||
branch: "chore-move-compute-gateway-to",
|
||||
repoName: "rivet-dev/skills",
|
||||
updatedAtMs: minutesAgo(2 * 24 * 60),
|
||||
status: "ready",
|
||||
}),
|
||||
sessions: [
|
||||
{
|
||||
id: "t9",
|
||||
sessionId: "t9",
|
||||
sessionName: "Gateway migration",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "idle",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1166,8 +1228,6 @@ export function buildInitialTasks(): Task[] {
|
|||
repoId: "sandbox-agent",
|
||||
title: "Fix broken auth middleware (error demo)",
|
||||
status: "error",
|
||||
runtimeStatus: "error",
|
||||
statusMessage: "session:error",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(2),
|
||||
branch: "fix/auth-middleware",
|
||||
|
|
@ -1178,7 +1238,7 @@ export function buildInitialTasks(): Task[] {
|
|||
sessionId: "status-error-session",
|
||||
sessionName: "Auth fix",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "error",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1197,9 +1257,7 @@ export function buildInitialTasks(): Task[] {
|
|||
id: "status-provisioning",
|
||||
repoId: "sandbox-agent",
|
||||
title: "Add rate limiting to API gateway (provisioning demo)",
|
||||
status: "new",
|
||||
runtimeStatus: "init_enqueue_provision",
|
||||
statusMessage: "Queueing sandbox provisioning.",
|
||||
status: "init_enqueue_provision",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(0),
|
||||
branch: null,
|
||||
|
|
@ -1211,7 +1269,7 @@ export function buildInitialTasks(): Task[] {
|
|||
sandboxSessionId: null,
|
||||
sessionName: "Session 1",
|
||||
agent: "Claude",
|
||||
model: "claude-sonnet-4",
|
||||
model: "sonnet",
|
||||
status: "pending_provision",
|
||||
thinkingSinceMs: null,
|
||||
unread: false,
|
||||
|
|
@ -1259,7 +1317,6 @@ export function buildInitialTasks(): Task[] {
|
|||
repoId: "sandbox-agent",
|
||||
title: "Refactor WebSocket handler (running demo)",
|
||||
status: "running",
|
||||
runtimeStatus: "running",
|
||||
repoName: "rivet-dev/sandbox-agent",
|
||||
updatedAtMs: minutesAgo(1),
|
||||
branch: "refactor/ws-handler",
|
||||
|
|
@ -1313,45 +1370,9 @@ function repoIdFromFullName(fullName: string): string {
|
|||
return parts[parts.length - 1] ?? fullName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build task entries from open PR fixture data.
|
||||
* Maps to the backend's PR sync behavior (RepositoryPrSyncActor) where PRs
|
||||
* appear as first-class sidebar items even without an associated task.
|
||||
* Each open PR gets a lightweight task entry so it shows in the sidebar.
|
||||
*/
|
||||
function buildPrTasks(): Task[] {
|
||||
// Collect branch names already claimed by hand-written tasks so we don't duplicate
|
||||
const existingBranches = new Set(
|
||||
buildInitialTasks()
|
||||
.map((t) => t.branch)
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
return rivetDevFixture.openPullRequests
|
||||
.filter((pr) => !existingBranches.has(pr.headRefName))
|
||||
.map((pr) => {
|
||||
const repoId = repoIdFromFullName(pr.repoFullName);
|
||||
return {
|
||||
id: `pr-${repoId}-${pr.number}`,
|
||||
repoId,
|
||||
title: pr.title,
|
||||
status: "idle" as const,
|
||||
repoName: pr.repoFullName,
|
||||
updatedAtMs: new Date(pr.updatedAt).getTime(),
|
||||
branch: pr.headRefName,
|
||||
pullRequest: { number: pr.number, status: pr.draft ? ("draft" as const) : ("ready" as const) },
|
||||
sessions: [],
|
||||
fileChanges: [],
|
||||
diffs: {},
|
||||
fileTree: [],
|
||||
minutesUsed: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function buildInitialMockLayoutViewModel(): TaskWorkspaceSnapshot {
|
||||
const repos = buildMockRepos();
|
||||
const tasks = [...buildInitialTasks(), ...buildPrTasks()];
|
||||
const tasks = buildInitialTasks();
|
||||
return {
|
||||
organizationId: "default",
|
||||
repos,
|
||||
|
|
|
|||
|
|
@ -80,9 +80,10 @@ function parseHistoryPayload(event: HistoryEvent): Record<string, unknown> {
|
|||
}
|
||||
}
|
||||
|
||||
async function debugDump(client: ReturnType<typeof createBackendClient>, organizationId: string, taskId: string): Promise<string> {
|
||||
async function debugDump(client: ReturnType<typeof createBackendClient>, organizationId: string, repoId: string, taskId: string): Promise<string> {
|
||||
try {
|
||||
const task = await client.getTask(organizationId, taskId);
|
||||
const task = await client.getTask(organizationId, repoId, taskId);
|
||||
const detail = await client.getTaskDetail(organizationId, repoId, taskId).catch(() => null);
|
||||
const history = await client.listHistory({ organizationId, taskId, limit: 80 }).catch(() => []);
|
||||
const historySummary = history
|
||||
.slice(0, 20)
|
||||
|
|
@ -90,10 +91,11 @@ async function debugDump(client: ReturnType<typeof createBackendClient>, organiz
|
|||
.join("\n");
|
||||
|
||||
let sessionEventsSummary = "";
|
||||
if (task.activeSandboxId && task.activeSessionId) {
|
||||
const activeSessionId = detail?.activeSessionId ?? null;
|
||||
if (task.activeSandboxId && activeSessionId) {
|
||||
const events = await client
|
||||
.listSandboxSessionEvents(organizationId, task.sandboxProviderId, task.activeSandboxId, {
|
||||
sessionId: task.activeSessionId,
|
||||
sessionId: activeSessionId,
|
||||
limit: 50,
|
||||
})
|
||||
.then((r) => r.items)
|
||||
|
|
@ -109,13 +111,11 @@ async function debugDump(client: ReturnType<typeof createBackendClient>, organiz
|
|||
JSON.stringify(
|
||||
{
|
||||
status: task.status,
|
||||
statusMessage: task.statusMessage,
|
||||
title: task.title,
|
||||
branchName: task.branchName,
|
||||
activeSandboxId: task.activeSandboxId,
|
||||
activeSessionId: task.activeSessionId,
|
||||
prUrl: task.prUrl,
|
||||
prSubmitted: task.prSubmitted,
|
||||
activeSessionId,
|
||||
pullRequestUrl: detail?.pullRequest?.url ?? null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
|
@ -189,7 +189,7 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
// Cold local sandbox startup can exceed a few minutes on first run.
|
||||
8 * 60_000,
|
||||
1_000,
|
||||
async () => client.getTask(organizationId, created.taskId),
|
||||
async () => client.getTask(organizationId, repo.repoId, created.taskId),
|
||||
(h) => Boolean(h.title && h.branchName && h.activeSandboxId),
|
||||
(h) => {
|
||||
if (h.status !== lastStatus) {
|
||||
|
|
@ -200,18 +200,18 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
}
|
||||
},
|
||||
).catch(async (err) => {
|
||||
const dump = await debugDump(client, organizationId, created.taskId);
|
||||
const dump = await debugDump(client, organizationId, repo.repoId, created.taskId);
|
||||
throw new Error(`${err instanceof Error ? err.message : String(err)}\n${dump}`);
|
||||
});
|
||||
|
||||
branchName = namedAndProvisioned.branchName!;
|
||||
sandboxId = namedAndProvisioned.activeSandboxId!;
|
||||
|
||||
const withSession = await poll<TaskRecord>(
|
||||
const withSession = await poll<Awaited<ReturnType<typeof client.getTaskDetail>>>(
|
||||
"task to create active session",
|
||||
3 * 60_000,
|
||||
1_500,
|
||||
async () => client.getTask(organizationId, created.taskId),
|
||||
async () => client.getTaskDetail(organizationId, repo.repoId, created.taskId),
|
||||
(h) => Boolean(h.activeSessionId),
|
||||
(h) => {
|
||||
if (h.status === "error") {
|
||||
|
|
@ -219,7 +219,7 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
}
|
||||
},
|
||||
).catch(async (err) => {
|
||||
const dump = await debugDump(client, organizationId, created.taskId);
|
||||
const dump = await debugDump(client, organizationId, repo.repoId, created.taskId);
|
||||
throw new Error(`${err instanceof Error ? err.message : String(err)}\n${dump}`);
|
||||
});
|
||||
|
||||
|
|
@ -231,14 +231,14 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
2_000,
|
||||
async () =>
|
||||
(
|
||||
await client.listSandboxSessionEvents(organizationId, withSession.sandboxProviderId, sandboxId!, {
|
||||
await client.listSandboxSessionEvents(organizationId, namedAndProvisioned.sandboxProviderId, sandboxId!, {
|
||||
sessionId: sessionId!,
|
||||
limit: 40,
|
||||
})
|
||||
).items,
|
||||
(events) => events.length > 0,
|
||||
).catch(async (err) => {
|
||||
const dump = await debugDump(client, organizationId, created.taskId);
|
||||
const dump = await debugDump(client, organizationId, repo.repoId, created.taskId);
|
||||
throw new Error(`${err instanceof Error ? err.message : String(err)}\n${dump}`);
|
||||
});
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
"task to reach idle state",
|
||||
8 * 60_000,
|
||||
2_000,
|
||||
async () => client.getTask(organizationId, created.taskId),
|
||||
async () => client.getTask(organizationId, repo.repoId, created.taskId),
|
||||
(h) => h.status === "idle",
|
||||
(h) => {
|
||||
if (h.status === "error") {
|
||||
|
|
@ -254,7 +254,7 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
}
|
||||
},
|
||||
).catch(async (err) => {
|
||||
const dump = await debugDump(client, organizationId, created.taskId);
|
||||
const dump = await debugDump(client, organizationId, repo.repoId, created.taskId);
|
||||
throw new Error(`${err instanceof Error ? err.message : String(err)}\n${dump}`);
|
||||
});
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
(events) => events.some((e) => e.kind === "task.pr_created"),
|
||||
)
|
||||
.catch(async (err) => {
|
||||
const dump = await debugDump(client, organizationId, created.taskId);
|
||||
const dump = await debugDump(client, organizationId, repo.repoId, created.taskId);
|
||||
throw new Error(`${err instanceof Error ? err.message : String(err)}\n${dump}`);
|
||||
})
|
||||
.then((events) => events.find((e) => e.kind === "task.pr_created")!);
|
||||
|
|
@ -287,16 +287,16 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
expect(prFiles.some((f) => f.filename === expectedFile)).toBe(true);
|
||||
|
||||
// Close the task and assert the sandbox is released (stopped).
|
||||
await client.runAction(organizationId, created.taskId, "archive");
|
||||
await client.runAction(organizationId, repo.repoId, created.taskId, "archive");
|
||||
|
||||
await poll<TaskRecord>(
|
||||
await poll<Awaited<ReturnType<typeof client.getTaskDetail>>>(
|
||||
"task to become archived (session released)",
|
||||
60_000,
|
||||
1_000,
|
||||
async () => client.getTask(organizationId, created.taskId),
|
||||
async () => client.getTaskDetail(organizationId, repo.repoId, created.taskId),
|
||||
(h) => h.status === "archived" && h.activeSessionId === null,
|
||||
).catch(async (err) => {
|
||||
const dump = await debugDump(client, organizationId, created.taskId);
|
||||
const dump = await debugDump(client, organizationId, repo.repoId, created.taskId);
|
||||
throw new Error(`${err instanceof Error ? err.message : String(err)}\n${dump}`);
|
||||
});
|
||||
|
||||
|
|
@ -311,7 +311,7 @@ describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
|||
return st.includes("destroyed") || st.includes("stopped") || st.includes("suspended") || st.includes("paused");
|
||||
},
|
||||
).catch(async (err) => {
|
||||
const dump = await debugDump(client, organizationId, created.taskId);
|
||||
const dump = await debugDump(client, organizationId, repo.repoId, created.taskId);
|
||||
const state = await client.sandboxProviderState(organizationId, "local", sandboxId!).catch(() => null);
|
||||
throw new Error(`${err instanceof Error ? err.message : String(err)}\n` + `sandbox state: ${state ? state.state : "unknown"}\n` + `${dump}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,19 +15,7 @@ function requiredEnv(name: string): string {
|
|||
|
||||
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||
const value = process.env[name]?.trim();
|
||||
switch (value) {
|
||||
case "claude-sonnet-4":
|
||||
case "claude-opus-4":
|
||||
case "gpt-5.3-codex":
|
||||
case "gpt-5.4":
|
||||
case "gpt-5.2-codex":
|
||||
case "gpt-5.1-codex-max":
|
||||
case "gpt-5.2":
|
||||
case "gpt-5.1-codex-mini":
|
||||
return value;
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
return value && value.length > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -28,19 +28,7 @@ function requiredEnv(name: string): string {
|
|||
|
||||
function workspaceModelEnv(name: string, fallback: WorkspaceModelId): WorkspaceModelId {
|
||||
const value = process.env[name]?.trim();
|
||||
switch (value) {
|
||||
case "claude-sonnet-4":
|
||||
case "claude-opus-4":
|
||||
case "gpt-5.3-codex":
|
||||
case "gpt-5.4":
|
||||
case "gpt-5.2-codex":
|
||||
case "gpt-5.1-codex-max":
|
||||
case "gpt-5.2":
|
||||
case "gpt-5.1-codex-mini":
|
||||
return value;
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
return value && value.length > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
function intEnv(name: string, fallback: number): number {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,20 @@ class FakeActorConn implements ActorConn {
|
|||
function organizationSnapshot(): OrganizationSummarySnapshot {
|
||||
return {
|
||||
organizationId: "org-1",
|
||||
github: {
|
||||
connectedAccount: "octocat",
|
||||
installationStatus: "connected",
|
||||
syncStatus: "synced",
|
||||
importedRepoCount: 1,
|
||||
lastSyncLabel: "Synced just now",
|
||||
lastSyncAt: 10,
|
||||
lastWebhookAt: null,
|
||||
lastWebhookEvent: "",
|
||||
syncGeneration: 1,
|
||||
syncPhase: null,
|
||||
processedRepositoryCount: 1,
|
||||
totalRepositoryCount: 1,
|
||||
},
|
||||
repos: [{ id: "repo-1", label: "repo-1", taskCount: 1, latestActivityMs: 10 }],
|
||||
taskSummaries: [
|
||||
{
|
||||
|
|
@ -61,10 +75,10 @@ function organizationSnapshot(): OrganizationSummarySnapshot {
|
|||
updatedAtMs: 10,
|
||||
branch: "main",
|
||||
pullRequest: null,
|
||||
activeSessionId: null,
|
||||
sessionsSummary: [],
|
||||
},
|
||||
],
|
||||
openPullRequests: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +132,20 @@ describe("RemoteSubscriptionManager", () => {
|
|||
type: "organizationUpdated",
|
||||
snapshot: {
|
||||
organizationId: "org-1",
|
||||
github: {
|
||||
connectedAccount: "octocat",
|
||||
installationStatus: "connected",
|
||||
syncStatus: "syncing",
|
||||
importedRepoCount: 1,
|
||||
lastSyncLabel: "Syncing repositories...",
|
||||
lastSyncAt: 10,
|
||||
lastWebhookAt: null,
|
||||
lastWebhookEvent: "",
|
||||
syncGeneration: 2,
|
||||
syncPhase: "syncing_branches",
|
||||
processedRepositoryCount: 1,
|
||||
totalRepositoryCount: 3,
|
||||
},
|
||||
repos: [],
|
||||
taskSummaries: [
|
||||
{
|
||||
|
|
@ -129,10 +157,10 @@ describe("RemoteSubscriptionManager", () => {
|
|||
updatedAtMs: 20,
|
||||
branch: "feature/live",
|
||||
pullRequest: null,
|
||||
activeSessionId: null,
|
||||
sessionsSummary: [],
|
||||
},
|
||||
],
|
||||
openPullRequests: [],
|
||||
},
|
||||
} satisfies OrganizationEvent);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ const sample: TaskRecord = {
|
|||
task: "Do test",
|
||||
sandboxProviderId: "local",
|
||||
status: "running",
|
||||
statusMessage: null,
|
||||
activeSandboxId: "sandbox-1",
|
||||
activeSessionId: "session-1",
|
||||
pullRequest: null,
|
||||
sandboxes: [
|
||||
{
|
||||
sandboxId: "sandbox-1",
|
||||
|
|
@ -26,17 +25,6 @@ const sample: TaskRecord = {
|
|||
updatedAt: 1,
|
||||
},
|
||||
],
|
||||
agentType: null,
|
||||
prSubmitted: false,
|
||||
diffStat: null,
|
||||
prUrl: null,
|
||||
prAuthor: null,
|
||||
ciStatus: null,
|
||||
reviewStatus: null,
|
||||
reviewer: null,
|
||||
conflictsWithMain: null,
|
||||
hasUnpushed: null,
|
||||
parentBranch: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue