mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-20 19:02:13 +00:00
parent
400f9a214e
commit
99abb9d42e
171 changed files with 7260 additions and 7342 deletions
|
|
@ -1,51 +1,51 @@
|
|||
import type { TaskStatus, ProviderId } from "@sandbox-agent/foundry-shared";
|
||||
import type { TaskStatus, SandboxProviderId } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
export interface TaskCreatedEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
providerId: ProviderId;
|
||||
sandboxProviderId: SandboxProviderId;
|
||||
branchName: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface TaskStatusEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
status: TaskStatus;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ProjectSnapshotEvent {
|
||||
workspaceId: string;
|
||||
export interface RepositorySnapshotEvent {
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface AgentStartedEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface AgentIdleEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface AgentErrorEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PrCreatedEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
prNumber: number;
|
||||
|
|
@ -53,7 +53,7 @@ export interface PrCreatedEvent {
|
|||
}
|
||||
|
||||
export interface PrClosedEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
prNumber: number;
|
||||
|
|
@ -61,7 +61,7 @@ export interface PrClosedEvent {
|
|||
}
|
||||
|
||||
export interface PrReviewEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
prNumber: number;
|
||||
|
|
@ -70,7 +70,7 @@ export interface PrReviewEvent {
|
|||
}
|
||||
|
||||
export interface CiStatusChangedEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
prNumber: number;
|
||||
|
|
@ -81,7 +81,7 @@ export type TaskStepName = "auto_commit" | "push" | "pr_submit";
|
|||
export type TaskStepStatus = "started" | "completed" | "skipped" | "failed";
|
||||
|
||||
export interface TaskStepEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
step: TaskStepName;
|
||||
|
|
@ -90,23 +90,15 @@ export interface TaskStepEvent {
|
|||
}
|
||||
|
||||
export interface BranchSwitchedEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
branchName: string;
|
||||
}
|
||||
|
||||
export interface SessionAttachedEvent {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface BranchSyncedEvent {
|
||||
workspaceId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
branchName: string;
|
||||
strategy: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,18 @@ const journal = {
|
|||
tag: "0000_github_data",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1773810002000,
|
||||
tag: "0001_default_branch",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 2,
|
||||
when: 1773810300000,
|
||||
tag: "0002_github_branches",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -56,6 +68,16 @@ CREATE TABLE \`github_pull_requests\` (
|
|||
\`is_draft\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `ALTER TABLE \`github_repositories\` ADD \`default_branch\` text NOT NULL DEFAULT 'main';
|
||||
`,
|
||||
m0002: `CREATE TABLE \`github_branches\` (
|
||||
\`branch_id\` text PRIMARY KEY NOT NULL,
|
||||
\`repo_id\` text NOT NULL,
|
||||
\`branch_name\` text NOT NULL,
|
||||
\`commit_sha\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ export const githubRepositories = sqliteTable("github_repositories", {
|
|||
fullName: text("full_name").notNull(),
|
||||
cloneUrl: text("clone_url").notNull(),
|
||||
private: integer("private").notNull(),
|
||||
defaultBranch: text("default_branch").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const githubBranches = sqliteTable("github_branches", {
|
||||
branchId: text("branch_id").notNull().primaryKey(),
|
||||
repoId: text("repo_id").notNull(),
|
||||
branchName: text("branch_name").notNull(),
|
||||
commitSha: text("commit_sha").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ import { eq } from "drizzle-orm";
|
|||
import { actor } from "rivetkit";
|
||||
import type { FoundryOrganization } from "@sandbox-agent/foundry-shared";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getOrCreateWorkspace, getTask } from "../handles.js";
|
||||
import { getOrCreateOrganization, getTask } from "../handles.js";
|
||||
import { repoIdFromRemote } from "../../services/repo.js";
|
||||
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
||||
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
||||
import { githubDataDb } from "./db/db.js";
|
||||
import { githubMembers, githubMeta, githubPullRequests, githubRepositories } from "./db/schema.js";
|
||||
import { githubBranches, githubMembers, githubMeta, githubPullRequests, githubRepositories } from "./db/schema.js";
|
||||
|
||||
const META_ROW_ID = 1;
|
||||
|
||||
interface GithubDataInput {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
interface GithubMemberRecord {
|
||||
|
|
@ -28,6 +28,13 @@ interface GithubRepositoryRecord {
|
|||
fullName: string;
|
||||
cloneUrl: string;
|
||||
private: boolean;
|
||||
defaultBranch: string;
|
||||
}
|
||||
|
||||
interface GithubBranchRecord {
|
||||
repoId: string;
|
||||
branchName: string;
|
||||
commitSha: string;
|
||||
}
|
||||
|
||||
interface GithubPullRequestRecord {
|
||||
|
|
@ -156,21 +163,21 @@ async function writeMeta(c: any, patch: Partial<Awaited<ReturnType<typeof readMe
|
|||
}
|
||||
|
||||
async function getOrganizationContext(c: any, overrides?: FullSyncInput) {
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
const organization = await workspace.getOrganizationShellStateIfInitialized({});
|
||||
if (!organization) {
|
||||
throw new Error(`Workspace ${c.state.workspaceId} is not initialized`);
|
||||
const organizationHandle = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
const organizationState = await organizationHandle.getOrganizationShellStateIfInitialized({});
|
||||
if (!organizationState) {
|
||||
throw new Error(`Organization ${c.state.organizationId} is not initialized`);
|
||||
}
|
||||
const auth = await resolveWorkspaceGithubAuth(c, c.state.workspaceId);
|
||||
const auth = await resolveOrganizationGithubAuth(c, c.state.organizationId);
|
||||
return {
|
||||
kind: overrides?.kind ?? organization.snapshot.kind,
|
||||
githubLogin: overrides?.githubLogin ?? organization.githubLogin,
|
||||
connectedAccount: overrides?.connectedAccount ?? organization.snapshot.github.connectedAccount ?? organization.githubLogin,
|
||||
installationId: overrides?.installationId ?? organization.githubInstallationId ?? null,
|
||||
kind: overrides?.kind ?? organizationState.snapshot.kind,
|
||||
githubLogin: overrides?.githubLogin ?? organizationState.githubLogin,
|
||||
connectedAccount: overrides?.connectedAccount ?? organizationState.snapshot.github.connectedAccount ?? organizationState.githubLogin,
|
||||
installationId: overrides?.installationId ?? organizationState.githubInstallationId ?? null,
|
||||
installationStatus:
|
||||
overrides?.installationStatus ??
|
||||
organization.snapshot.github.installationStatus ??
|
||||
(organization.snapshot.kind === "personal" ? "connected" : "reconnect_required"),
|
||||
organizationState.snapshot.github.installationStatus ??
|
||||
(organizationState.snapshot.kind === "personal" ? "connected" : "reconnect_required"),
|
||||
accessToken: overrides?.accessToken ?? auth?.githubToken ?? null,
|
||||
};
|
||||
}
|
||||
|
|
@ -185,6 +192,23 @@ async function replaceRepositories(c: any, repositories: GithubRepositoryRecord[
|
|||
fullName: repository.fullName,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
private: repository.private ? 1 : 0,
|
||||
defaultBranch: repository.defaultBranch,
|
||||
updatedAt,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceBranches(c: any, branches: GithubBranchRecord[], updatedAt: number) {
|
||||
await c.db.delete(githubBranches).run();
|
||||
for (const branch of branches) {
|
||||
await c.db
|
||||
.insert(githubBranches)
|
||||
.values({
|
||||
branchId: `${branch.repoId}:${branch.branchName}`,
|
||||
repoId: branch.repoId,
|
||||
branchName: branch.branchName,
|
||||
commitSha: branch.commitSha,
|
||||
updatedAt,
|
||||
})
|
||||
.run();
|
||||
|
|
@ -234,12 +258,12 @@ async function replacePullRequests(c: any, pullRequests: GithubPullRequestRecord
|
|||
}
|
||||
|
||||
async function refreshTaskSummaryForBranch(c: any, repoId: string, branchName: string) {
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
await workspace.refreshTaskSummaryForGithubBranch({ repoId, branchName });
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.refreshTaskSummaryForGithubBranch({ repoId, branchName });
|
||||
}
|
||||
|
||||
async function emitPullRequestChangeEvents(c: any, beforeRows: any[], afterRows: any[]) {
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
const beforeById = new Map(beforeRows.map((row) => [row.prId, row]));
|
||||
const afterById = new Map(afterRows.map((row) => [row.prId, row]));
|
||||
|
||||
|
|
@ -258,7 +282,7 @@ async function emitPullRequestChangeEvents(c: any, beforeRows: any[], afterRows:
|
|||
if (!changed) {
|
||||
continue;
|
||||
}
|
||||
await workspace.applyOpenPullRequestUpdate({
|
||||
await organization.applyOpenPullRequestUpdate({
|
||||
pullRequest: pullRequestSummaryFromRow(row),
|
||||
});
|
||||
await refreshTaskSummaryForBranch(c, row.repoId, row.headRefName);
|
||||
|
|
@ -268,14 +292,14 @@ async function emitPullRequestChangeEvents(c: any, beforeRows: any[], afterRows:
|
|||
if (afterById.has(prId)) {
|
||||
continue;
|
||||
}
|
||||
await workspace.removeOpenPullRequest({ prId });
|
||||
await organization.removeOpenPullRequest({ prId });
|
||||
await refreshTaskSummaryForBranch(c, row.repoId, row.headRefName);
|
||||
}
|
||||
}
|
||||
|
||||
async function autoArchiveTaskForClosedPullRequest(c: any, row: any) {
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
const match = await workspace.findTaskForGithubBranch({
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
const match = await organization.findTaskForGithubBranch({
|
||||
repoId: row.repoId,
|
||||
branchName: row.headRefName,
|
||||
});
|
||||
|
|
@ -283,7 +307,7 @@ async function autoArchiveTaskForClosedPullRequest(c: any, row: any) {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const task = getTask(c, c.state.workspaceId, row.repoId, match.taskId);
|
||||
const task = getTask(c, c.state.organizationId, row.repoId, match.taskId);
|
||||
await task.archive({ reason: `PR ${String(row.state).toLowerCase()}` });
|
||||
} catch {
|
||||
// Best-effort only. Task summary refresh will still clear the PR state.
|
||||
|
|
@ -391,6 +415,69 @@ async function resolvePullRequests(
|
|||
}));
|
||||
}
|
||||
|
||||
async function listRepositoryBranchesForContext(
|
||||
context: Awaited<ReturnType<typeof getOrganizationContext>>,
|
||||
repository: GithubRepositoryRecord,
|
||||
): Promise<GithubBranchRecord[]> {
|
||||
const { appShell } = getActorRuntimeContext();
|
||||
let branches: Array<{ name: string; commitSha: string }> = [];
|
||||
|
||||
if (context.installationId != null) {
|
||||
try {
|
||||
branches = await appShell.github.listInstallationRepositoryBranches(context.installationId, repository.fullName);
|
||||
} catch (error) {
|
||||
if (!context.accessToken) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (branches.length === 0 && context.accessToken) {
|
||||
branches = await appShell.github.listUserRepositoryBranches(context.accessToken, repository.fullName);
|
||||
}
|
||||
|
||||
const repoId = repoIdFromRemote(repository.cloneUrl);
|
||||
return branches.map((branch) => ({
|
||||
repoId,
|
||||
branchName: branch.name,
|
||||
commitSha: branch.commitSha,
|
||||
}));
|
||||
}
|
||||
|
||||
async function resolveBranches(
|
||||
_c: any,
|
||||
context: Awaited<ReturnType<typeof getOrganizationContext>>,
|
||||
repositories: GithubRepositoryRecord[],
|
||||
): Promise<GithubBranchRecord[]> {
|
||||
return (await Promise.all(repositories.map((repository) => listRepositoryBranchesForContext(context, repository)))).flat();
|
||||
}
|
||||
|
||||
async function refreshRepositoryBranches(
|
||||
c: any,
|
||||
context: Awaited<ReturnType<typeof getOrganizationContext>>,
|
||||
repository: GithubRepositoryRecord,
|
||||
updatedAt: number,
|
||||
): Promise<void> {
|
||||
const nextBranches = await listRepositoryBranchesForContext(context, repository);
|
||||
await c.db
|
||||
.delete(githubBranches)
|
||||
.where(eq(githubBranches.repoId, repoIdFromRemote(repository.cloneUrl)))
|
||||
.run();
|
||||
|
||||
for (const branch of nextBranches) {
|
||||
await c.db
|
||||
.insert(githubBranches)
|
||||
.values({
|
||||
branchId: `${branch.repoId}:${branch.branchName}`,
|
||||
repoId: branch.repoId,
|
||||
branchName: branch.branchName,
|
||||
commitSha: branch.commitSha,
|
||||
updatedAt,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
async function readAllPullRequestRows(c: any) {
|
||||
return await c.db.select().from(githubPullRequests).all();
|
||||
}
|
||||
|
|
@ -409,15 +496,17 @@ async function runFullSync(c: any, input: FullSyncInput = {}) {
|
|||
});
|
||||
|
||||
const repositories = await resolveRepositories(c, context);
|
||||
const branches = await resolveBranches(c, context, repositories);
|
||||
const members = await resolveMembers(c, context);
|
||||
const pullRequests = await resolvePullRequests(c, context, repositories);
|
||||
|
||||
await replaceRepositories(c, repositories, startedAt);
|
||||
await replaceBranches(c, branches, startedAt);
|
||||
await replaceMembers(c, members, startedAt);
|
||||
await replacePullRequests(c, pullRequests);
|
||||
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
await workspace.applyGithubDataProjection({
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.applyGithubDataProjection({
|
||||
connectedAccount: context.connectedAccount,
|
||||
installationStatus: context.installationStatus,
|
||||
installationId: context.installationId,
|
||||
|
|
@ -455,16 +544,18 @@ export const githubData = actor({
|
|||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, input: GithubDataInput) => ({
|
||||
workspaceId: input.workspaceId,
|
||||
organizationId: input.organizationId,
|
||||
}),
|
||||
actions: {
|
||||
async getSummary(c) {
|
||||
const repositories = await c.db.select().from(githubRepositories).all();
|
||||
const branches = await c.db.select().from(githubBranches).all();
|
||||
const members = await c.db.select().from(githubMembers).all();
|
||||
const pullRequests = await c.db.select().from(githubPullRequests).all();
|
||||
return {
|
||||
...(await readMeta(c)),
|
||||
repositoryCount: repositories.length,
|
||||
branchCount: branches.length,
|
||||
memberCount: members.length,
|
||||
pullRequestCount: pullRequests.length,
|
||||
};
|
||||
|
|
@ -477,14 +568,39 @@ export const githubData = actor({
|
|||
fullName: row.fullName,
|
||||
cloneUrl: row.cloneUrl,
|
||||
private: Boolean(row.private),
|
||||
defaultBranch: row.defaultBranch,
|
||||
}));
|
||||
},
|
||||
|
||||
async getRepository(c, input: { repoId: string }) {
|
||||
const row = await c.db.select().from(githubRepositories).where(eq(githubRepositories.repoId, input.repoId)).get();
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
repoId: row.repoId,
|
||||
fullName: row.fullName,
|
||||
cloneUrl: row.cloneUrl,
|
||||
private: Boolean(row.private),
|
||||
defaultBranch: row.defaultBranch,
|
||||
};
|
||||
},
|
||||
|
||||
async listPullRequestsForRepository(c, input: { repoId: string }) {
|
||||
const rows = await c.db.select().from(githubPullRequests).where(eq(githubPullRequests.repoId, input.repoId)).all();
|
||||
return rows.map(pullRequestSummaryFromRow);
|
||||
},
|
||||
|
||||
async listBranchesForRepository(c, input: { repoId: string }) {
|
||||
const rows = await c.db.select().from(githubBranches).where(eq(githubBranches.repoId, input.repoId)).all();
|
||||
return rows
|
||||
.map((row) => ({
|
||||
branchName: row.branchName,
|
||||
commitSha: row.commitSha,
|
||||
}))
|
||||
.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
||||
},
|
||||
|
||||
async listOpenPullRequests(c) {
|
||||
const rows = await c.db.select().from(githubPullRequests).all();
|
||||
return rows.map(pullRequestSummaryFromRow).sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
||||
|
|
@ -539,6 +655,7 @@ export const githubData = actor({
|
|||
fullName: repository.fullName,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
private: repository.private ? 1 : 0,
|
||||
defaultBranch: repository.defaultBranch,
|
||||
updatedAt,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
|
|
@ -547,13 +664,25 @@ export const githubData = actor({
|
|||
fullName: repository.fullName,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
private: repository.private ? 1 : 0,
|
||||
defaultBranch: repository.defaultBranch,
|
||||
updatedAt,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
await refreshRepositoryBranches(
|
||||
c,
|
||||
context,
|
||||
{
|
||||
fullName: repository.fullName,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
private: repository.private,
|
||||
defaultBranch: repository.defaultBranch,
|
||||
},
|
||||
updatedAt,
|
||||
);
|
||||
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
await workspace.applyGithubRepositoryProjection({
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.applyGithubRepositoryProjection({
|
||||
repoId: input.repoId,
|
||||
remoteUrl: repository.cloneUrl,
|
||||
});
|
||||
|
|
@ -562,6 +691,7 @@ export const githubData = actor({
|
|||
fullName: repository.fullName,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
private: repository.private,
|
||||
defaultBranch: repository.defaultBranch,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -656,6 +786,7 @@ export const githubData = actor({
|
|||
async clearState(c, input: ClearStateInput) {
|
||||
const beforeRows = await readAllPullRequestRows(c);
|
||||
await c.db.delete(githubPullRequests).run();
|
||||
await c.db.delete(githubBranches).run();
|
||||
await c.db.delete(githubRepositories).run();
|
||||
await c.db.delete(githubMembers).run();
|
||||
await writeMeta(c, {
|
||||
|
|
@ -667,8 +798,8 @@ export const githubData = actor({
|
|||
lastSyncAt: null,
|
||||
});
|
||||
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
await workspace.applyGithubDataProjection({
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.applyGithubDataProjection({
|
||||
connectedAccount: input.connectedAccount,
|
||||
installationStatus: input.installationStatus,
|
||||
installationId: input.installationId,
|
||||
|
|
@ -683,6 +814,7 @@ export const githubData = actor({
|
|||
async handlePullRequestWebhook(c, input: PullRequestWebhookInput) {
|
||||
const beforeRows = await readAllPullRequestRows(c);
|
||||
const repoId = repoIdFromRemote(input.repository.cloneUrl);
|
||||
const currentRepository = await c.db.select().from(githubRepositories).where(eq(githubRepositories.repoId, repoId)).get();
|
||||
const updatedAt = Date.now();
|
||||
const state = normalizePrStatus(input.pullRequest);
|
||||
const prId = `${repoId}#${input.pullRequest.number}`;
|
||||
|
|
@ -694,6 +826,7 @@ export const githubData = actor({
|
|||
fullName: input.repository.fullName,
|
||||
cloneUrl: input.repository.cloneUrl,
|
||||
private: input.repository.private ? 1 : 0,
|
||||
defaultBranch: currentRepository?.defaultBranch ?? input.pullRequest.baseRefName ?? "main",
|
||||
updatedAt,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
|
|
@ -702,6 +835,7 @@ export const githubData = actor({
|
|||
fullName: input.repository.fullName,
|
||||
cloneUrl: input.repository.cloneUrl,
|
||||
private: input.repository.private ? 1 : 0,
|
||||
defaultBranch: currentRepository?.defaultBranch ?? input.pullRequest.baseRefName ?? "main",
|
||||
updatedAt,
|
||||
},
|
||||
})
|
||||
|
|
@ -753,8 +887,8 @@ export const githubData = actor({
|
|||
lastSyncAt: updatedAt,
|
||||
});
|
||||
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
await workspace.applyGithubRepositoryProjection({
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.applyGithubRepositoryProjection({
|
||||
repoId,
|
||||
remoteUrl: input.repository.cloneUrl,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { authUserKey, githubDataKey, taskKey, historyKey, projectBranchSyncKey, projectKey, taskSandboxKey, workspaceKey } from "./keys.js";
|
||||
import { authUserKey, githubDataKey, historyKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "./keys.js";
|
||||
|
||||
export function actorClient(c: any) {
|
||||
return c.client();
|
||||
}
|
||||
|
||||
export async function getOrCreateWorkspace(c: any, workspaceId: string) {
|
||||
return await actorClient(c).workspace.getOrCreate(workspaceKey(workspaceId), {
|
||||
createWithInput: workspaceId,
|
||||
export async function getOrCreateOrganization(c: any, organizationId: string) {
|
||||
return await actorClient(c).organization.getOrCreate(organizationKey(organizationId), {
|
||||
createWithInput: organizationId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -20,76 +20,61 @@ export function getAuthUser(c: any, userId: string) {
|
|||
return actorClient(c).authUser.get(authUserKey(userId));
|
||||
}
|
||||
|
||||
export async function getOrCreateProject(c: any, workspaceId: string, repoId: string, remoteUrl: string) {
|
||||
return await actorClient(c).project.getOrCreate(projectKey(workspaceId, repoId), {
|
||||
export async function getOrCreateRepository(c: any, organizationId: string, repoId: string, remoteUrl: string) {
|
||||
return await actorClient(c).repository.getOrCreate(repositoryKey(organizationId, repoId), {
|
||||
createWithInput: {
|
||||
workspaceId,
|
||||
organizationId,
|
||||
repoId,
|
||||
remoteUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getProject(c: any, workspaceId: string, repoId: string) {
|
||||
return actorClient(c).project.get(projectKey(workspaceId, repoId));
|
||||
export function getRepository(c: any, organizationId: string, repoId: string) {
|
||||
return actorClient(c).repository.get(repositoryKey(organizationId, repoId));
|
||||
}
|
||||
|
||||
export function getTask(c: any, workspaceId: string, repoId: string, taskId: string) {
|
||||
return actorClient(c).task.get(taskKey(workspaceId, repoId, taskId));
|
||||
export function getTask(c: any, organizationId: string, repoId: string, taskId: string) {
|
||||
return actorClient(c).task.get(taskKey(organizationId, repoId, taskId));
|
||||
}
|
||||
|
||||
export async function getOrCreateTask(c: any, workspaceId: string, repoId: string, taskId: string, createWithInput: Record<string, unknown>) {
|
||||
return await actorClient(c).task.getOrCreate(taskKey(workspaceId, repoId, taskId), {
|
||||
export async function getOrCreateTask(c: any, organizationId: string, repoId: string, taskId: string, createWithInput: Record<string, unknown>) {
|
||||
return await actorClient(c).task.getOrCreate(taskKey(organizationId, repoId, taskId), {
|
||||
createWithInput,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOrCreateHistory(c: any, workspaceId: string, repoId: string) {
|
||||
return await actorClient(c).history.getOrCreate(historyKey(workspaceId, repoId), {
|
||||
export async function getOrCreateHistory(c: any, organizationId: string, repoId: string) {
|
||||
return await actorClient(c).history.getOrCreate(historyKey(organizationId, repoId), {
|
||||
createWithInput: {
|
||||
workspaceId,
|
||||
organizationId,
|
||||
repoId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOrCreateGithubData(c: any, workspaceId: string) {
|
||||
return await actorClient(c).githubData.getOrCreate(githubDataKey(workspaceId), {
|
||||
export async function getOrCreateGithubData(c: any, organizationId: string) {
|
||||
return await actorClient(c).githubData.getOrCreate(githubDataKey(organizationId), {
|
||||
createWithInput: {
|
||||
workspaceId,
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getGithubData(c: any, workspaceId: string) {
|
||||
return actorClient(c).githubData.get(githubDataKey(workspaceId));
|
||||
export function getGithubData(c: any, organizationId: string) {
|
||||
return actorClient(c).githubData.get(githubDataKey(organizationId));
|
||||
}
|
||||
|
||||
export async function getOrCreateProjectBranchSync(c: any, workspaceId: string, repoId: string, repoPath: string, intervalMs: number) {
|
||||
return await actorClient(c).projectBranchSync.getOrCreate(projectBranchSyncKey(workspaceId, repoId), {
|
||||
createWithInput: {
|
||||
workspaceId,
|
||||
repoId,
|
||||
repoPath,
|
||||
intervalMs,
|
||||
},
|
||||
});
|
||||
export function getTaskSandbox(c: any, organizationId: string, sandboxId: string) {
|
||||
return actorClient(c).taskSandbox.get(taskSandboxKey(organizationId, sandboxId));
|
||||
}
|
||||
|
||||
export function getTaskSandbox(c: any, workspaceId: string, sandboxId: string) {
|
||||
return actorClient(c).taskSandbox.get(taskSandboxKey(workspaceId, sandboxId));
|
||||
}
|
||||
|
||||
export async function getOrCreateTaskSandbox(c: any, workspaceId: string, sandboxId: string, createWithInput?: Record<string, unknown>) {
|
||||
return await actorClient(c).taskSandbox.getOrCreate(taskSandboxKey(workspaceId, sandboxId), {
|
||||
export async function getOrCreateTaskSandbox(c: any, organizationId: string, sandboxId: string, createWithInput?: Record<string, unknown>) {
|
||||
return await actorClient(c).taskSandbox.getOrCreate(taskSandboxKey(organizationId, sandboxId), {
|
||||
createWithInput,
|
||||
});
|
||||
}
|
||||
|
||||
export function selfProjectBranchSync(c: any) {
|
||||
return actorClient(c).projectBranchSync.getForId(c.actorId);
|
||||
}
|
||||
|
||||
export function selfHistory(c: any) {
|
||||
return actorClient(c).history.getForId(c.actorId);
|
||||
}
|
||||
|
|
@ -98,12 +83,12 @@ export function selfTask(c: any) {
|
|||
return actorClient(c).task.getForId(c.actorId);
|
||||
}
|
||||
|
||||
export function selfWorkspace(c: any) {
|
||||
return actorClient(c).workspace.getForId(c.actorId);
|
||||
export function selfOrganization(c: any) {
|
||||
return actorClient(c).organization.getForId(c.actorId);
|
||||
}
|
||||
|
||||
export function selfProject(c: any) {
|
||||
return actorClient(c).project.getForId(c.actorId);
|
||||
export function selfRepository(c: any) {
|
||||
return actorClient(c).repository.getForId(c.actorId);
|
||||
}
|
||||
|
||||
export function selfAuthUser(c: any) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { historyDb } from "./db/db.js";
|
|||
import { events } from "./db/schema.js";
|
||||
|
||||
export interface HistoryInput {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ export const history = actor({
|
|||
icon: "database",
|
||||
},
|
||||
createState: (_c, input: HistoryInput) => ({
|
||||
workspaceId: input.workspaceId,
|
||||
organizationId: input.organizationId,
|
||||
repoId: input.repoId,
|
||||
}),
|
||||
actions: {
|
||||
|
|
@ -106,7 +106,7 @@ export const history = actor({
|
|||
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: c.state.repoId,
|
||||
}));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ import { setup } from "rivetkit";
|
|||
import { githubData } from "./github-data/index.js";
|
||||
import { task } from "./task/index.js";
|
||||
import { history } from "./history/index.js";
|
||||
import { projectBranchSync } from "./project-branch-sync/index.js";
|
||||
import { project } from "./project/index.js";
|
||||
import { repository } from "./repository/index.js";
|
||||
import { taskSandbox } from "./sandbox/index.js";
|
||||
import { workspace } from "./workspace/index.js";
|
||||
import { organization } from "./organization/index.js";
|
||||
import { logger } from "../logging.js";
|
||||
|
||||
const RUNNER_VERSION = Math.floor(Date.now() / 1000);
|
||||
|
|
@ -23,13 +22,12 @@ export const registry = setup({
|
|||
},
|
||||
use: {
|
||||
authUser,
|
||||
workspace,
|
||||
project,
|
||||
organization,
|
||||
repository,
|
||||
task,
|
||||
taskSandbox,
|
||||
history,
|
||||
githubData,
|
||||
projectBranchSync,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -40,7 +38,6 @@ export * from "./github-data/index.js";
|
|||
export * from "./task/index.js";
|
||||
export * from "./history/index.js";
|
||||
export * from "./keys.js";
|
||||
export * from "./project-branch-sync/index.js";
|
||||
export * from "./project/index.js";
|
||||
export * from "./repository/index.js";
|
||||
export * from "./sandbox/index.js";
|
||||
export * from "./workspace/index.js";
|
||||
export * from "./organization/index.js";
|
||||
|
|
|
|||
|
|
@ -1,33 +1,29 @@
|
|||
export type ActorKey = string[];
|
||||
|
||||
export function workspaceKey(workspaceId: string): ActorKey {
|
||||
return ["ws", workspaceId];
|
||||
export function organizationKey(organizationId: string): ActorKey {
|
||||
return ["org", organizationId];
|
||||
}
|
||||
|
||||
export function authUserKey(userId: string): ActorKey {
|
||||
return ["ws", "app", "user", userId];
|
||||
return ["org", "app", "user", userId];
|
||||
}
|
||||
|
||||
export function projectKey(workspaceId: string, repoId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId];
|
||||
export function repositoryKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId];
|
||||
}
|
||||
|
||||
export function taskKey(workspaceId: string, repoId: string, taskId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId, "task", taskId];
|
||||
export function taskKey(organizationId: string, repoId: string, taskId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "task", taskId];
|
||||
}
|
||||
|
||||
export function taskSandboxKey(workspaceId: string, sandboxId: string): ActorKey {
|
||||
return ["ws", workspaceId, "sandbox", sandboxId];
|
||||
export function taskSandboxKey(organizationId: string, sandboxId: string): ActorKey {
|
||||
return ["org", organizationId, "sandbox", sandboxId];
|
||||
}
|
||||
|
||||
export function historyKey(workspaceId: string, repoId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId, "history"];
|
||||
export function historyKey(organizationId: string, repoId: string): ActorKey {
|
||||
return ["org", organizationId, "repository", repoId, "history"];
|
||||
}
|
||||
|
||||
export function githubDataKey(workspaceId: string): ActorKey {
|
||||
return ["ws", workspaceId, "github-data"];
|
||||
}
|
||||
|
||||
export function projectBranchSyncKey(workspaceId: string, repoId: string): ActorKey {
|
||||
return ["ws", workspaceId, "project", repoId, "branch-sync"];
|
||||
export function githubDataKey(organizationId: string): ActorKey {
|
||||
return ["org", organizationId, "github-data"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import { logger } from "../logging.js";
|
|||
|
||||
export function resolveErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
let msg = error.message;
|
||||
if (error.cause) {
|
||||
msg += ` [cause: ${resolveErrorMessage(error.cause)}]`;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
// @ts-nocheck
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { Loop } from "rivetkit/workflow";
|
||||
import type {
|
||||
AddRepoInput,
|
||||
CreateTaskInput,
|
||||
HistoryEvent,
|
||||
HistoryQueryInput,
|
||||
ListTasksInput,
|
||||
ProviderId,
|
||||
SandboxProviderId,
|
||||
RepoOverview,
|
||||
RepoRecord,
|
||||
RepoStackActionInput,
|
||||
RepoStackActionResult,
|
||||
StarSandboxAgentRepoInput,
|
||||
StarSandboxAgentRepoResult,
|
||||
SwitchResult,
|
||||
|
|
@ -26,37 +22,33 @@ import type {
|
|||
TaskWorkbenchSelectInput,
|
||||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchTabInput,
|
||||
TaskWorkbenchSessionInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
WorkbenchOpenPrSummary,
|
||||
WorkbenchRepoSummary,
|
||||
WorkbenchRepositorySummary,
|
||||
WorkbenchSessionSummary,
|
||||
WorkbenchTaskSummary,
|
||||
WorkspaceEvent,
|
||||
WorkspaceSummarySnapshot,
|
||||
WorkspaceUseInput,
|
||||
OrganizationEvent,
|
||||
OrganizationSummarySnapshot,
|
||||
OrganizationUseInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getGithubData, getOrCreateGithubData, getTask, getOrCreateHistory, getOrCreateProject, selfWorkspace } from "../handles.js";
|
||||
import { getGithubData, getOrCreateGithubData, getTask, getOrCreateHistory, getOrCreateRepository, selfOrganization } from "../handles.js";
|
||||
import { logActorWarning, resolveErrorMessage } from "../logging.js";
|
||||
import { availableSandboxProviderIds, defaultSandboxProviderId } from "../../sandbox-config.js";
|
||||
import { normalizeRemoteUrl, repoIdFromRemote } from "../../services/repo.js";
|
||||
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
||||
import { organizationProfile, taskLookup, repos, providerProfiles, taskSummaries } from "./db/schema.js";
|
||||
import { defaultSandboxProviderId } from "../../sandbox-config.js";
|
||||
import { repoIdFromRemote } from "../../services/repo.js";
|
||||
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
||||
import { organizationProfile, taskLookup, repos, taskSummaries } from "./db/schema.js";
|
||||
import { agentTypeForModel } from "../task/workbench.js";
|
||||
import { expectQueueResponse } from "../../services/queue.js";
|
||||
import { workspaceAppActions } from "./app-shell.js";
|
||||
import { organizationAppActions } from "./app-shell.js";
|
||||
|
||||
interface WorkspaceState {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
interface RefreshProviderProfilesCommand {
|
||||
providerId?: ProviderId;
|
||||
interface OrganizationState {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
interface GetTaskInput {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
|
|
@ -65,32 +57,30 @@ interface TaskProxyActionInput extends GetTaskInput {
|
|||
}
|
||||
|
||||
interface RepoOverviewInput {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
}
|
||||
|
||||
const WORKSPACE_QUEUE_NAMES = [
|
||||
"workspace.command.addRepo",
|
||||
"workspace.command.createTask",
|
||||
"workspace.command.refreshProviderProfiles",
|
||||
"workspace.command.syncGithubOrganizationRepos",
|
||||
"workspace.command.syncGithubSession",
|
||||
const ORGANIZATION_QUEUE_NAMES = [
|
||||
"organization.command.createTask",
|
||||
"organization.command.syncGithubOrganizationRepos",
|
||||
"organization.command.syncGithubSession",
|
||||
] as const;
|
||||
const SANDBOX_AGENT_REPO = "rivet-dev/sandbox-agent";
|
||||
|
||||
type WorkspaceQueueName = (typeof WORKSPACE_QUEUE_NAMES)[number];
|
||||
type OrganizationQueueName = (typeof ORGANIZATION_QUEUE_NAMES)[number];
|
||||
|
||||
export { WORKSPACE_QUEUE_NAMES };
|
||||
export { ORGANIZATION_QUEUE_NAMES };
|
||||
|
||||
export function workspaceWorkflowQueueName(name: WorkspaceQueueName): WorkspaceQueueName {
|
||||
export function organizationWorkflowQueueName(name: OrganizationQueueName): OrganizationQueueName {
|
||||
return name;
|
||||
}
|
||||
|
||||
const ORGANIZATION_PROFILE_ROW_ID = "profile";
|
||||
|
||||
function assertWorkspace(c: { state: WorkspaceState }, workspaceId: string): void {
|
||||
if (workspaceId !== c.state.workspaceId) {
|
||||
throw new Error(`Workspace actor mismatch: actor=${c.state.workspaceId} command=${workspaceId}`);
|
||||
function assertOrganization(c: { state: OrganizationState }, organizationId: string): void {
|
||||
if (organizationId !== c.state.organizationId) {
|
||||
throw new Error(`Organization actor mismatch: actor=${c.state.organizationId} command=${organizationId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,12 +126,12 @@ async function collectAllTaskSummaries(c: any): Promise<TaskSummary[]> {
|
|||
const all: TaskSummary[] = [];
|
||||
for (const row of repoRows) {
|
||||
try {
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, row.repoId, row.remoteUrl);
|
||||
const snapshot = await project.listTaskSummaries({ includeArchived: true });
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
||||
const snapshot = await repository.listTaskSummaries({ includeArchived: true });
|
||||
all.push(...snapshot);
|
||||
} catch (error) {
|
||||
logActorWarning("workspace", "failed collecting tasks for repo", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
logActorWarning("organization", "failed collecting tasks for repo", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
|
|
@ -166,7 +156,7 @@ function repoLabelFromRemote(remoteUrl: string): string {
|
|||
return remoteUrl;
|
||||
}
|
||||
|
||||
function buildRepoSummary(repoRow: { repoId: string; remoteUrl: string; updatedAt: number }, taskRows: WorkbenchTaskSummary[]): WorkbenchRepoSummary {
|
||||
function buildRepoSummary(repoRow: { repoId: string; remoteUrl: string; updatedAt: number }, taskRows: WorkbenchTaskSummary[]): WorkbenchRepositorySummary {
|
||||
const repoTasks = taskRows.filter((task) => task.repoId === repoRow.repoId);
|
||||
const latestActivityMs = repoTasks.reduce((latest, task) => Math.max(latest, task.updatedAtMs), repoRow.updatedAt);
|
||||
|
||||
|
|
@ -207,14 +197,14 @@ function taskSummaryFromRow(row: any): WorkbenchTaskSummary {
|
|||
}
|
||||
|
||||
async function listOpenPullRequestsSnapshot(c: any, taskRows: WorkbenchTaskSummary[]): Promise<WorkbenchOpenPrSummary[]> {
|
||||
const githubData = getGithubData(c, c.state.workspaceId);
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
const openPullRequests = await githubData.listOpenPullRequests({}).catch(() => []);
|
||||
const claimedBranches = new Set(taskRows.filter((task) => task.branch).map((task) => `${task.repoId}:${task.branch}`));
|
||||
|
||||
return openPullRequests.filter((pullRequest: WorkbenchOpenPrSummary) => !claimedBranches.has(`${pullRequest.repoId}:${pullRequest.headRefName}`));
|
||||
}
|
||||
|
||||
async function reconcileWorkbenchProjection(c: any): Promise<WorkspaceSummarySnapshot> {
|
||||
async function reconcileWorkbenchProjection(c: any): Promise<OrganizationSummarySnapshot> {
|
||||
const repoRows = await c.db
|
||||
.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, updatedAt: repos.updatedAt })
|
||||
.from(repos)
|
||||
|
|
@ -224,12 +214,12 @@ async function reconcileWorkbenchProjection(c: any): Promise<WorkspaceSummarySna
|
|||
const taskRows: WorkbenchTaskSummary[] = [];
|
||||
for (const row of repoRows) {
|
||||
try {
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, row.repoId, row.remoteUrl);
|
||||
const summaries = await project.listTaskSummaries({ includeArchived: true });
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, row.repoId, row.remoteUrl);
|
||||
const summaries = await repository.listTaskSummaries({ includeArchived: true });
|
||||
for (const summary of summaries) {
|
||||
try {
|
||||
await upsertTaskLookupRow(c, summary.taskId, row.repoId);
|
||||
const task = getTask(c, c.state.workspaceId, row.repoId, summary.taskId);
|
||||
const task = getTask(c, c.state.organizationId, row.repoId, summary.taskId);
|
||||
const taskSummary = await task.getTaskSummary({});
|
||||
taskRows.push(taskSummary);
|
||||
await c.db
|
||||
|
|
@ -241,8 +231,8 @@ async function reconcileWorkbenchProjection(c: any): Promise<WorkspaceSummarySna
|
|||
})
|
||||
.run();
|
||||
} catch (error) {
|
||||
logActorWarning("workspace", "failed collecting task summary during reconciliation", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
logActorWarning("organization", "failed collecting task summary during reconciliation", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
taskId: summary.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
|
|
@ -250,8 +240,8 @@ async function reconcileWorkbenchProjection(c: any): Promise<WorkspaceSummarySna
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logActorWarning("workspace", "failed collecting repo during workbench reconciliation", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
logActorWarning("organization", "failed collecting repo during workbench reconciliation", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
|
|
@ -260,7 +250,7 @@ async function reconcileWorkbenchProjection(c: any): Promise<WorkspaceSummarySna
|
|||
|
||||
taskRows.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
|
||||
return {
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId: c.state.organizationId,
|
||||
repos: repoRows.map((row) => buildRepoSummary(row, taskRows)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||
taskSummaries: taskRows,
|
||||
openPullRequests: await listOpenPullRequestsSnapshot(c, taskRows),
|
||||
|
|
@ -269,33 +259,15 @@ async function reconcileWorkbenchProjection(c: any): Promise<WorkspaceSummarySna
|
|||
|
||||
async function requireWorkbenchTask(c: any, taskId: string) {
|
||||
const repoId = await resolveRepoId(c, taskId);
|
||||
return getTask(c, c.state.workspaceId, repoId, taskId);
|
||||
}
|
||||
|
||||
async function waitForWorkbenchTaskReady(task: any, timeoutMs = 5 * 60_000): Promise<any> {
|
||||
const startedAt = Date.now();
|
||||
|
||||
for (;;) {
|
||||
const record = await task.get();
|
||||
if (record?.branchName && record?.title) {
|
||||
return record;
|
||||
}
|
||||
if (record?.status === "error") {
|
||||
throw new Error("task initialization failed before the workbench session was ready");
|
||||
}
|
||||
if (Date.now() - startedAt > timeoutMs) {
|
||||
throw new Error("timed out waiting for task initialization");
|
||||
}
|
||||
await delay(1_000);
|
||||
}
|
||||
return getTask(c, c.state.organizationId, repoId, taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the workspace sidebar snapshot from the workspace actor's local SQLite
|
||||
* Reads the organization sidebar snapshot from the organization actor's local SQLite
|
||||
* plus the org-scoped GitHub actor for open PRs. Task actors still push
|
||||
* summary updates into `task_summaries`, so the hot read path stays bounded.
|
||||
*/
|
||||
async function getWorkspaceSummarySnapshot(c: any): Promise<WorkspaceSummarySnapshot> {
|
||||
async function getOrganizationSummarySnapshot(c: any): Promise<OrganizationSummarySnapshot> {
|
||||
const repoRows = await c.db
|
||||
.select({
|
||||
repoId: repos.repoId,
|
||||
|
|
@ -309,7 +281,7 @@ async function getWorkspaceSummarySnapshot(c: any): Promise<WorkspaceSummarySnap
|
|||
const summaries = taskRows.map(taskSummaryFromRow);
|
||||
|
||||
return {
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId: c.state.organizationId,
|
||||
repos: repoRows.map((row) => buildRepoSummary(row, summaries)).sort((left, right) => right.latestActivityMs - left.latestActivityMs),
|
||||
taskSummaries: summaries,
|
||||
openPullRequests: await listOpenPullRequestsSnapshot(c, summaries),
|
||||
|
|
@ -323,61 +295,14 @@ async function broadcastRepoSummary(
|
|||
): Promise<void> {
|
||||
const matchingTaskRows = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, repoRow.repoId)).all();
|
||||
const repo = buildRepoSummary(repoRow, matchingTaskRows.map(taskSummaryFromRow));
|
||||
c.broadcast("workspaceUpdated", { type, repo } satisfies WorkspaceEvent);
|
||||
}
|
||||
|
||||
async function addRepoMutation(c: any, input: AddRepoInput): Promise<RepoRecord> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
|
||||
const remoteUrl = normalizeRemoteUrl(input.remoteUrl);
|
||||
if (!remoteUrl) {
|
||||
throw new Error("remoteUrl is required");
|
||||
}
|
||||
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const auth = await resolveWorkspaceGithubAuth(c, c.state.workspaceId);
|
||||
await driver.git.validateRemote(remoteUrl, { githubToken: auth?.githubToken ?? null });
|
||||
|
||||
const repoId = repoIdFromRemote(remoteUrl);
|
||||
const now = Date.now();
|
||||
const existing = await c.db.select({ repoId: repos.repoId }).from(repos).where(eq(repos.repoId, repoId)).get();
|
||||
|
||||
await c.db
|
||||
.insert(repos)
|
||||
.values({
|
||||
repoId,
|
||||
remoteUrl,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: repos.repoId,
|
||||
set: {
|
||||
remoteUrl,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
|
||||
await broadcastRepoSummary(c, existing ? "repoUpdated" : "repoAdded", {
|
||||
repoId,
|
||||
remoteUrl,
|
||||
updatedAt: now,
|
||||
});
|
||||
return {
|
||||
workspaceId: c.state.workspaceId,
|
||||
repoId,
|
||||
remoteUrl,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
c.broadcast("organizationUpdated", { type, repo } satisfies OrganizationEvent);
|
||||
}
|
||||
|
||||
async function createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskRecord> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const { config } = getActorRuntimeContext();
|
||||
const providerId = input.providerId ?? defaultSandboxProviderId(config);
|
||||
const sandboxProviderId = input.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
|
||||
const repoId = input.repoId;
|
||||
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, repoId)).get();
|
||||
|
|
@ -386,27 +311,11 @@ async function createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskR
|
|||
}
|
||||
const remoteUrl = repoRow.remoteUrl;
|
||||
|
||||
await c.db
|
||||
.insert(providerProfiles)
|
||||
.values({
|
||||
providerId,
|
||||
profileJson: JSON.stringify({ providerId }),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: providerProfiles.providerId,
|
||||
set: {
|
||||
profileJson: JSON.stringify({ providerId }),
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
})
|
||||
.run();
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, repoId, remoteUrl);
|
||||
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, repoId, remoteUrl);
|
||||
|
||||
const created = await project.createTask({
|
||||
const created = await repository.createTask({
|
||||
task: input.task,
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
agentType: input.agentType ?? null,
|
||||
explicitTitle: input.explicitTitle ?? null,
|
||||
explicitBranchName: input.explicitBranchName ?? null,
|
||||
|
|
@ -426,13 +335,13 @@ async function createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskR
|
|||
.run();
|
||||
|
||||
try {
|
||||
const task = getTask(c, c.state.workspaceId, repoId, created.taskId);
|
||||
await workspaceActions.applyTaskSummaryUpdate(c, {
|
||||
const task = getTask(c, c.state.organizationId, repoId, created.taskId);
|
||||
await organizationActions.applyTaskSummaryUpdate(c, {
|
||||
taskSummary: await task.getTaskSummary({}),
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("workspace", "failed seeding task summary after task creation", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
logActorWarning("organization", "failed seeding task summary after task creation", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId,
|
||||
taskId: created.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
|
|
@ -442,34 +351,10 @@ async function createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskR
|
|||
return created;
|
||||
}
|
||||
|
||||
async function refreshProviderProfilesMutation(c: any, command?: RefreshProviderProfilesCommand): Promise<void> {
|
||||
const body = command ?? {};
|
||||
const { config } = getActorRuntimeContext();
|
||||
const providerIds: ProviderId[] = body.providerId ? [body.providerId] : availableSandboxProviderIds(config);
|
||||
|
||||
for (const providerId of providerIds) {
|
||||
await c.db
|
||||
.insert(providerProfiles)
|
||||
.values({
|
||||
providerId,
|
||||
profileJson: JSON.stringify({ providerId }),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: providerProfiles.providerId,
|
||||
set: {
|
||||
profileJson: JSON.stringify({ providerId }),
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
|
||||
await ctx.loop("workspace-command-loop", async (loopCtx: any) => {
|
||||
const msg = await loopCtx.queue.next("next-workspace-command", {
|
||||
names: [...WORKSPACE_QUEUE_NAMES],
|
||||
export async function runOrganizationWorkflow(ctx: any): Promise<void> {
|
||||
await ctx.loop("organization-command-loop", async (loopCtx: any) => {
|
||||
const msg = await loopCtx.queue.next("next-organization-command", {
|
||||
names: [...ORGANIZATION_QUEUE_NAMES],
|
||||
completable: true,
|
||||
});
|
||||
if (!msg) {
|
||||
|
|
@ -477,19 +362,9 @@ export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
|
|||
}
|
||||
|
||||
try {
|
||||
if (msg.name === "workspace.command.addRepo") {
|
||||
if (msg.name === "organization.command.createTask") {
|
||||
const result = await loopCtx.step({
|
||||
name: "workspace-add-repo",
|
||||
timeout: 60_000,
|
||||
run: async () => addRepoMutation(loopCtx, msg.body as AddRepoInput),
|
||||
});
|
||||
await msg.complete(result);
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "workspace.command.createTask") {
|
||||
const result = await loopCtx.step({
|
||||
name: "workspace-create-task",
|
||||
name: "organization-create-task",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => createTaskMutation(loopCtx, msg.body as CreateTaskInput),
|
||||
});
|
||||
|
|
@ -497,17 +372,9 @@ export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
|
|||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "workspace.command.refreshProviderProfiles") {
|
||||
await loopCtx.step("workspace-refresh-provider-profiles", async () =>
|
||||
refreshProviderProfilesMutation(loopCtx, msg.body as RefreshProviderProfilesCommand),
|
||||
);
|
||||
await msg.complete({ ok: true });
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "workspace.command.syncGithubSession") {
|
||||
if (msg.name === "organization.command.syncGithubSession") {
|
||||
await loopCtx.step({
|
||||
name: "workspace-sync-github-session",
|
||||
name: "organization-sync-github-session",
|
||||
timeout: 60_000,
|
||||
run: async () => {
|
||||
const { syncGithubOrganizations } = await import("./app-shell.js");
|
||||
|
|
@ -518,9 +385,9 @@ export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
|
|||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "workspace.command.syncGithubOrganizationRepos") {
|
||||
if (msg.name === "organization.command.syncGithubOrganizationRepos") {
|
||||
await loopCtx.step({
|
||||
name: "workspace-sync-github-organization-repos",
|
||||
name: "organization-sync-github-organization-repos",
|
||||
timeout: 60_000,
|
||||
run: async () => {
|
||||
const { syncGithubOrganizationRepos } = await import("./app-shell.js");
|
||||
|
|
@ -532,14 +399,12 @@ export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
|
|||
}
|
||||
} catch (error) {
|
||||
const message = resolveErrorMessage(error);
|
||||
logActorWarning("workspace", "workspace workflow command failed", {
|
||||
workspaceId: loopCtx.state.workspaceId,
|
||||
logActorWarning("organization", "organization workflow command failed", {
|
||||
queueName: msg.name,
|
||||
error: message,
|
||||
});
|
||||
await msg.complete({ error: message }).catch((completeError: unknown) => {
|
||||
logActorWarning("workspace", "workspace workflow failed completing error response", {
|
||||
workspaceId: loopCtx.state.workspaceId,
|
||||
logActorWarning("organization", "organization workflow failed completing error response", {
|
||||
queueName: msg.name,
|
||||
error: resolveErrorMessage(completeError),
|
||||
});
|
||||
|
|
@ -550,25 +415,15 @@ export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export const workspaceActions = {
|
||||
...workspaceAppActions,
|
||||
async useWorkspace(c: any, input: WorkspaceUseInput): Promise<{ workspaceId: string }> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
return { workspaceId: c.state.workspaceId };
|
||||
export const organizationActions = {
|
||||
...organizationAppActions,
|
||||
async useOrganization(c: any, input: OrganizationUseInput): Promise<{ organizationId: string }> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
return { organizationId: c.state.organizationId };
|
||||
},
|
||||
|
||||
async addRepo(c: any, input: AddRepoInput): Promise<RepoRecord> {
|
||||
const self = selfWorkspace(c);
|
||||
return expectQueueResponse<RepoRecord>(
|
||||
await self.send(workspaceWorkflowQueueName("workspace.command.addRepo"), input, {
|
||||
wait: true,
|
||||
timeout: 60_000,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async listRepos(c: any, input: WorkspaceUseInput): Promise<RepoRecord[]> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
async listRepos(c: any, input: OrganizationUseInput): Promise<RepoRecord[]> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const rows = await c.db
|
||||
.select({
|
||||
|
|
@ -582,7 +437,7 @@ export const workspaceActions = {
|
|||
.all();
|
||||
|
||||
return rows.map((row) => ({
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
remoteUrl: row.remoteUrl,
|
||||
createdAt: row.createdAt,
|
||||
|
|
@ -591,19 +446,22 @@ export const workspaceActions = {
|
|||
},
|
||||
|
||||
async createTask(c: any, input: CreateTaskInput): Promise<TaskRecord> {
|
||||
const self = selfWorkspace(c);
|
||||
const self = selfOrganization(c);
|
||||
return expectQueueResponse<TaskRecord>(
|
||||
await self.send(workspaceWorkflowQueueName("workspace.command.createTask"), input, {
|
||||
await self.send(organizationWorkflowQueueName("organization.command.createTask"), input, {
|
||||
wait: true,
|
||||
timeout: 5 * 60_000,
|
||||
timeout: 10_000,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async starSandboxAgentRepo(c: any, input: StarSandboxAgentRepoInput): Promise<StarSandboxAgentRepoResult> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
const { driver } = getActorRuntimeContext();
|
||||
await driver.github.starRepository(SANDBOX_AGENT_REPO);
|
||||
const auth = await resolveOrganizationGithubAuth(c, c.state.organizationId);
|
||||
await driver.github.starRepository(SANDBOX_AGENT_REPO, {
|
||||
githubToken: auth?.githubToken ?? null,
|
||||
});
|
||||
return {
|
||||
repo: SANDBOX_AGENT_REPO,
|
||||
starredAt: Date.now(),
|
||||
|
|
@ -613,7 +471,7 @@ export const workspaceActions = {
|
|||
/**
|
||||
* Called by task actors when their summary-level state changes.
|
||||
* This is the write path for the local materialized projection; clients read
|
||||
* the projection via `getWorkspaceSummary`, but only task actors should push
|
||||
* the projection via `getOrganizationSummary`, but only task actors should push
|
||||
* rows into it.
|
||||
*/
|
||||
async applyTaskSummaryUpdate(c: any, input: { taskSummary: WorkbenchTaskSummary }): Promise<void> {
|
||||
|
|
@ -625,12 +483,12 @@ export const workspaceActions = {
|
|||
set: taskSummaryRowFromSummary(input.taskSummary),
|
||||
})
|
||||
.run();
|
||||
c.broadcast("workspaceUpdated", { type: "taskSummaryUpdated", taskSummary: input.taskSummary } satisfies WorkspaceEvent);
|
||||
c.broadcast("organizationUpdated", { type: "taskSummaryUpdated", taskSummary: input.taskSummary } satisfies OrganizationEvent);
|
||||
},
|
||||
|
||||
async removeTaskSummary(c: any, input: { taskId: string }): Promise<void> {
|
||||
await c.db.delete(taskSummaries).where(eq(taskSummaries.taskId, input.taskId)).run();
|
||||
c.broadcast("workspaceUpdated", { type: "taskRemoved", taskId: input.taskId } satisfies WorkspaceEvent);
|
||||
c.broadcast("organizationUpdated", { type: "taskRemoved", taskId: input.taskId } satisfies OrganizationEvent);
|
||||
},
|
||||
|
||||
async findTaskForGithubBranch(c: any, input: { repoId: string; branchName: string }): Promise<{ taskId: string | null }> {
|
||||
|
|
@ -645,13 +503,13 @@ export const workspaceActions = {
|
|||
|
||||
for (const summary of matches) {
|
||||
try {
|
||||
const task = getTask(c, c.state.workspaceId, input.repoId, summary.taskId);
|
||||
await workspaceActions.applyTaskSummaryUpdate(c, {
|
||||
const task = getTask(c, c.state.organizationId, input.repoId, summary.taskId);
|
||||
await organizationActions.applyTaskSummaryUpdate(c, {
|
||||
taskSummary: await task.getTaskSummary({}),
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("workspace", "failed refreshing task summary for GitHub branch", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
logActorWarning("organization", "failed refreshing task summary for GitHub branch", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: input.repoId,
|
||||
branchName: input.branchName,
|
||||
taskId: summary.taskId,
|
||||
|
|
@ -666,11 +524,11 @@ export const workspaceActions = {
|
|||
if (summaries.some((summary) => summary.branch === input.pullRequest.headRefName)) {
|
||||
return;
|
||||
}
|
||||
c.broadcast("workspaceUpdated", { type: "pullRequestUpdated", pullRequest: input.pullRequest } satisfies WorkspaceEvent);
|
||||
c.broadcast("organizationUpdated", { type: "pullRequestUpdated", pullRequest: input.pullRequest } satisfies OrganizationEvent);
|
||||
},
|
||||
|
||||
async removeOpenPullRequest(c: any, input: { prId: string }): Promise<void> {
|
||||
c.broadcast("workspaceUpdated", { type: "pullRequestRemoved", prId: input.prId } satisfies WorkspaceEvent);
|
||||
c.broadcast("organizationUpdated", { type: "pullRequestRemoved", prId: input.prId } satisfies OrganizationEvent);
|
||||
},
|
||||
|
||||
async applyGithubRepositoryProjection(c: any, input: { repoId: string; remoteUrl: string }): Promise<void> {
|
||||
|
|
@ -747,7 +605,7 @@ export const workspaceActions = {
|
|||
continue;
|
||||
}
|
||||
await c.db.delete(repos).where(eq(repos.repoId, repo.repoId)).run();
|
||||
c.broadcast("workspaceUpdated", { type: "repoRemoved", repoId: repo.repoId } satisfies WorkspaceEvent);
|
||||
c.broadcast("organizationUpdated", { type: "repoRemoved", repoId: repo.repoId } satisfies OrganizationEvent);
|
||||
}
|
||||
|
||||
const profile = await c.db
|
||||
|
|
@ -775,13 +633,13 @@ export const workspaceActions = {
|
|||
async recordGithubWebhookReceipt(
|
||||
c: any,
|
||||
input: {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
event: string;
|
||||
action?: string | null;
|
||||
receivedAt?: number;
|
||||
},
|
||||
): Promise<void> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const profile = await c.db
|
||||
.select({ id: organizationProfile.id })
|
||||
|
|
@ -802,45 +660,38 @@ export const workspaceActions = {
|
|||
.run();
|
||||
},
|
||||
|
||||
async getWorkspaceSummary(c: any, input: WorkspaceUseInput): Promise<WorkspaceSummarySnapshot> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
return await getWorkspaceSummarySnapshot(c);
|
||||
async getOrganizationSummary(c: any, input: OrganizationUseInput): Promise<OrganizationSummarySnapshot> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
return await getOrganizationSummarySnapshot(c);
|
||||
},
|
||||
|
||||
async reconcileWorkbenchState(c: any, input: WorkspaceUseInput): Promise<WorkspaceSummarySnapshot> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
async reconcileWorkbenchState(c: any, input: OrganizationUseInput): Promise<OrganizationSummarySnapshot> {
|
||||
assertOrganization(c, input.organizationId);
|
||||
return await reconcileWorkbenchProjection(c);
|
||||
},
|
||||
|
||||
async createWorkbenchTask(c: any, input: TaskWorkbenchCreateTaskInput): Promise<{ taskId: string; tabId?: string }> {
|
||||
const created = await workspaceActions.createTask(c, {
|
||||
workspaceId: c.state.workspaceId,
|
||||
async createWorkbenchTask(c: any, input: TaskWorkbenchCreateTaskInput): Promise<{ taskId: string; sessionId?: string }> {
|
||||
// Step 1: Create the task record (wait: true — local state mutations only).
|
||||
const created = await organizationActions.createTask(c, {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: input.repoId,
|
||||
task: input.task,
|
||||
...(input.title ? { explicitTitle: input.title } : {}),
|
||||
...(input.onBranch ? { onBranch: input.onBranch } : input.branch ? { explicitBranchName: input.branch } : {}),
|
||||
...(input.model ? { agentType: agentTypeForModel(input.model) } : {}),
|
||||
});
|
||||
|
||||
// Step 2: Enqueue session creation + initial message (wait: false).
|
||||
// The task workflow creates the session record and sends the message in
|
||||
// the background. The client observes progress via push events on the
|
||||
// task subscription topic.
|
||||
const task = await requireWorkbenchTask(c, created.taskId);
|
||||
await waitForWorkbenchTaskReady(task);
|
||||
const session = await task.createWorkbenchSession({
|
||||
taskId: created.taskId,
|
||||
...(input.model ? { model: input.model } : {}),
|
||||
});
|
||||
await task.sendWorkbenchMessage({
|
||||
taskId: created.taskId,
|
||||
tabId: session.tabId,
|
||||
await task.createWorkbenchSessionAndSend({
|
||||
model: input.model,
|
||||
text: input.task,
|
||||
attachments: [],
|
||||
waitForCompletion: true,
|
||||
});
|
||||
await task.getSessionDetail({
|
||||
sessionId: session.tabId,
|
||||
});
|
||||
return {
|
||||
taskId: created.taskId,
|
||||
tabId: session.tabId,
|
||||
};
|
||||
|
||||
return { taskId: created.taskId };
|
||||
},
|
||||
|
||||
async markWorkbenchUnread(c: any, input: TaskWorkbenchSelectInput): Promise<void> {
|
||||
|
|
@ -858,7 +709,7 @@ export const workspaceActions = {
|
|||
await task.renameWorkbenchBranch(input);
|
||||
},
|
||||
|
||||
async createWorkbenchSession(c: any, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ tabId: string }> {
|
||||
async createWorkbenchSession(c: any, input: TaskWorkbenchSelectInput & { model?: string }): Promise<{ sessionId: string }> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
return await task.createWorkbenchSession({ ...(input.model ? { model: input.model } : {}) });
|
||||
},
|
||||
|
|
@ -888,12 +739,12 @@ export const workspaceActions = {
|
|||
await task.sendWorkbenchMessage(input);
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(c: any, input: TaskWorkbenchTabInput): Promise<void> {
|
||||
async stopWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.stopWorkbenchSession(input);
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(c: any, input: TaskWorkbenchTabInput): Promise<void> {
|
||||
async closeWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise<void> {
|
||||
const task = await requireWorkbenchTask(c, input.taskId);
|
||||
await task.closeWorkbenchSession(input);
|
||||
},
|
||||
|
|
@ -909,23 +760,23 @@ export const workspaceActions = {
|
|||
},
|
||||
|
||||
async reloadGithubOrganization(c: any): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.workspaceId).reloadOrganization({});
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadOrganization({});
|
||||
},
|
||||
|
||||
async reloadGithubPullRequests(c: any): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.workspaceId).reloadAllPullRequests({});
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadAllPullRequests({});
|
||||
},
|
||||
|
||||
async reloadGithubRepository(c: any, input: { repoId: string }): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.workspaceId).reloadRepository(input);
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadRepository(input);
|
||||
},
|
||||
|
||||
async reloadGithubPullRequest(c: any, input: { repoId: string; prNumber: number }): Promise<void> {
|
||||
await getOrCreateGithubData(c, c.state.workspaceId).reloadPullRequest(input);
|
||||
await getOrCreateGithubData(c, c.state.organizationId).reloadPullRequest(input);
|
||||
},
|
||||
|
||||
async listTasks(c: any, input: ListTasksInput): Promise<TaskSummary[]> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
if (input.repoId) {
|
||||
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, input.repoId)).get();
|
||||
|
|
@ -933,67 +784,41 @@ export const workspaceActions = {
|
|||
throw new Error(`Unknown repo: ${input.repoId}`);
|
||||
}
|
||||
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, input.repoId, repoRow.remoteUrl);
|
||||
return await project.listTaskSummaries({ includeArchived: true });
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, input.repoId, repoRow.remoteUrl);
|
||||
return await repository.listTaskSummaries({ includeArchived: true });
|
||||
}
|
||||
|
||||
return await collectAllTaskSummaries(c);
|
||||
},
|
||||
|
||||
async getRepoOverview(c: any, input: RepoOverviewInput): Promise<RepoOverview> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, input.repoId)).get();
|
||||
if (!repoRow) {
|
||||
throw new Error(`Unknown repo: ${input.repoId}`);
|
||||
}
|
||||
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, input.repoId, repoRow.remoteUrl);
|
||||
await project.ensure({ remoteUrl: repoRow.remoteUrl });
|
||||
return await project.getRepoOverview({});
|
||||
},
|
||||
|
||||
async runRepoStackAction(c: any, input: RepoStackActionInput): Promise<RepoStackActionResult> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
|
||||
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, input.repoId)).get();
|
||||
if (!repoRow) {
|
||||
throw new Error(`Unknown repo: ${input.repoId}`);
|
||||
}
|
||||
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, input.repoId, repoRow.remoteUrl);
|
||||
await project.ensure({ remoteUrl: repoRow.remoteUrl });
|
||||
return await project.runRepoStackAction({
|
||||
action: input.action,
|
||||
branchName: input.branchName,
|
||||
parentBranch: input.parentBranch,
|
||||
});
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, input.repoId, repoRow.remoteUrl);
|
||||
return await repository.getRepoOverview({});
|
||||
},
|
||||
|
||||
async switchTask(c: any, taskId: string): Promise<SwitchResult> {
|
||||
const repoId = await resolveRepoId(c, taskId);
|
||||
const h = getTask(c, c.state.workspaceId, repoId, taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, taskId);
|
||||
const record = await h.get();
|
||||
const switched = await h.switch();
|
||||
|
||||
return {
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId: c.state.organizationId,
|
||||
taskId,
|
||||
providerId: record.providerId,
|
||||
sandboxProviderId: record.sandboxProviderId,
|
||||
switchTarget: switched.switchTarget,
|
||||
};
|
||||
},
|
||||
|
||||
async refreshProviderProfiles(c: any, command?: RefreshProviderProfilesCommand): Promise<void> {
|
||||
const self = selfWorkspace(c);
|
||||
await self.send(workspaceWorkflowQueueName("workspace.command.refreshProviderProfiles"), command ?? {}, {
|
||||
wait: true,
|
||||
timeout: 60_000,
|
||||
});
|
||||
},
|
||||
|
||||
async history(c: any, input: HistoryQueryInput): Promise<HistoryEvent[]> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const limit = input.limit ?? 20;
|
||||
const repoRows = await c.db.select({ repoId: repos.repoId }).from(repos).all();
|
||||
|
|
@ -1002,7 +827,7 @@ export const workspaceActions = {
|
|||
|
||||
for (const row of repoRows) {
|
||||
try {
|
||||
const hist = await getOrCreateHistory(c, c.state.workspaceId, row.repoId);
|
||||
const hist = await getOrCreateHistory(c, c.state.organizationId, row.repoId);
|
||||
const items = await hist.list({
|
||||
branch: input.branch,
|
||||
taskId: input.taskId,
|
||||
|
|
@ -1010,8 +835,8 @@ export const workspaceActions = {
|
|||
});
|
||||
allEvents.push(...items);
|
||||
} catch (error) {
|
||||
logActorWarning("workspace", "history lookup failed for repo", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
logActorWarning("organization", "history lookup failed for repo", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: row.repoId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
|
|
@ -1023,7 +848,7 @@ export const workspaceActions = {
|
|||
},
|
||||
|
||||
async getTask(c: any, input: GetTaskInput): Promise<TaskRecord> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
|
||||
|
|
@ -1032,49 +857,49 @@ export const workspaceActions = {
|
|||
throw new Error(`Unknown repo: ${repoId}`);
|
||||
}
|
||||
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, repoId, repoRow.remoteUrl);
|
||||
return await project.getTaskEnriched({ taskId: input.taskId });
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, repoId, repoRow.remoteUrl);
|
||||
return await repository.getTaskEnriched({ taskId: input.taskId });
|
||||
},
|
||||
|
||||
async attachTask(c: any, input: TaskProxyActionInput): Promise<{ target: string; sessionId: string | null }> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.workspaceId, repoId, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
return await h.attach({ reason: input.reason });
|
||||
},
|
||||
|
||||
async pushTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.workspaceId, repoId, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.push({ reason: input.reason });
|
||||
},
|
||||
|
||||
async syncTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.workspaceId, repoId, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.sync({ reason: input.reason });
|
||||
},
|
||||
|
||||
async mergeTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.workspaceId, repoId, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.merge({ reason: input.reason });
|
||||
},
|
||||
|
||||
async archiveTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.workspaceId, repoId, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.archive({ reason: input.reason });
|
||||
},
|
||||
|
||||
async killTask(c: any, input: TaskProxyActionInput): Promise<void> {
|
||||
assertWorkspace(c, input.workspaceId);
|
||||
assertOrganization(c, input.organizationId);
|
||||
const repoId = await resolveRepoId(c, input.taskId);
|
||||
const h = getTask(c, c.state.workspaceId, repoId, input.taskId);
|
||||
const h = getTask(c, c.state.organizationId, repoId, input.taskId);
|
||||
await h.kill({ reason: input.reason });
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,4 +2,4 @@ import { db } from "rivetkit/db/drizzle";
|
|||
import * as schema from "./schema.js";
|
||||
import migrations from "./migrations.js";
|
||||
|
||||
export const projectDb = db({ schema, migrations });
|
||||
export const organizationDb = db({ schema, migrations });
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from "rivetkit/db/drizzle";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/actors/organization/db/drizzle",
|
||||
schema: "./src/actors/organization/db/schema.ts",
|
||||
});
|
||||
|
|
@ -69,12 +69,6 @@ CREATE TABLE `organization_profile` (
|
|||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `provider_profiles` (
|
||||
`provider_id` text PRIMARY KEY NOT NULL,
|
||||
`profile_json` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `repos` (
|
||||
`repo_id` text PRIMARY KEY NOT NULL,
|
||||
`remote_url` text NOT NULL,
|
||||
|
|
@ -457,37 +457,6 @@
|
|||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"provider_profiles": {
|
||||
"name": "provider_profiles",
|
||||
"columns": {
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"profile_json": {
|
||||
"name": "profile_json",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repos": {
|
||||
"name": "repos",
|
||||
"columns": {
|
||||
|
|
@ -22,6 +22,12 @@ const journal = {
|
|||
tag: "0002_task_summaries",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 3,
|
||||
when: 1773810001000,
|
||||
tag: "0003_drop_provider_profiles",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -99,12 +105,6 @@ CREATE TABLE \`organization_profile\` (
|
|||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`provider_profiles\` (
|
||||
\`provider_id\` text PRIMARY KEY NOT NULL,
|
||||
\`profile_json\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`repos\` (
|
||||
\`repo_id\` text PRIMARY KEY NOT NULL,
|
||||
\`remote_url\` text NOT NULL,
|
||||
|
|
@ -170,6 +170,8 @@ CREATE TABLE IF NOT EXISTS \`auth_verification\` (
|
|||
\`pull_request_json\` text,
|
||||
\`sessions_summary_json\` text DEFAULT '[]' NOT NULL
|
||||
);
|
||||
`,
|
||||
m0003: `DROP TABLE IF EXISTS \`provider_profiles\`;
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
|
||||
// SQLite is per workspace actor instance, so no workspaceId column needed.
|
||||
export const providerProfiles = sqliteTable("provider_profiles", {
|
||||
providerId: text("provider_id").notNull().primaryKey(),
|
||||
// Structured by the provider profile snapshot returned by provider integrations.
|
||||
profileJson: text("profile_json").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
// SQLite is per organization actor instance, so no organizationId column needed.
|
||||
|
||||
export const repos = sqliteTable("repos", {
|
||||
repoId: text("repo_id").notNull().primaryKey(),
|
||||
|
|
@ -23,7 +17,7 @@ export const taskLookup = sqliteTable("task_lookup", {
|
|||
/**
|
||||
* Materialized sidebar projection maintained by task actors.
|
||||
* The source of truth still lives on each task actor; this table exists so
|
||||
* workspace reads can stay local and avoid fan-out across child actors.
|
||||
* organization reads can stay local and avoid fan-out across child actors.
|
||||
*/
|
||||
export const taskSummaries = sqliteTable("task_summaries", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
19
foundry/packages/backend/src/actors/organization/index.ts
Normal file
19
foundry/packages/backend/src/actors/organization/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { actor, queue } from "rivetkit";
|
||||
import { workflow } from "rivetkit/workflow";
|
||||
import { organizationDb } from "./db/db.js";
|
||||
import { runOrganizationWorkflow, ORGANIZATION_QUEUE_NAMES, organizationActions } from "./actions.js";
|
||||
|
||||
export const organization = actor({
|
||||
db: organizationDb,
|
||||
queues: Object.fromEntries(ORGANIZATION_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||
options: {
|
||||
name: "Organization",
|
||||
icon: "compass",
|
||||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, organizationId: string) => ({
|
||||
organizationId,
|
||||
}),
|
||||
actions: organizationActions,
|
||||
run: workflow(runOrganizationWorkflow),
|
||||
});
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
import { actor, queue } from "rivetkit";
|
||||
import { workflow } from "rivetkit/workflow";
|
||||
import type { GitDriver } from "../../driver.js";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getProject, selfProjectBranchSync } from "../handles.js";
|
||||
import { logActorWarning, resolveErrorMessage, resolveErrorStack } from "../logging.js";
|
||||
import { type PollingControlState, runWorkflowPollingLoop } from "../polling.js";
|
||||
import { parentLookupFromStack } from "../project/stack-model.js";
|
||||
import { withRepoGitLock } from "../../services/repo-git-lock.js";
|
||||
|
||||
export interface ProjectBranchSyncInput {
|
||||
workspaceId: string;
|
||||
repoId: string;
|
||||
repoPath: string;
|
||||
intervalMs: number;
|
||||
}
|
||||
|
||||
interface SetIntervalCommand {
|
||||
intervalMs: number;
|
||||
}
|
||||
|
||||
interface EnrichedBranchSnapshot {
|
||||
branchName: string;
|
||||
commitSha: string;
|
||||
parentBranch: string | null;
|
||||
trackedInStack: boolean;
|
||||
diffStat: string | null;
|
||||
hasUnpushed: boolean;
|
||||
conflictsWithMain: boolean;
|
||||
}
|
||||
|
||||
interface ProjectBranchSyncState extends PollingControlState {
|
||||
workspaceId: string;
|
||||
repoId: string;
|
||||
repoPath: string;
|
||||
}
|
||||
|
||||
const CONTROL = {
|
||||
start: "project.branch_sync.control.start",
|
||||
stop: "project.branch_sync.control.stop",
|
||||
setInterval: "project.branch_sync.control.set_interval",
|
||||
force: "project.branch_sync.control.force",
|
||||
} as const;
|
||||
|
||||
async function enrichBranches(workspaceId: string, repoId: string, repoPath: string, git: GitDriver): Promise<EnrichedBranchSnapshot[]> {
|
||||
return await withRepoGitLock(repoPath, async () => {
|
||||
await git.fetch(repoPath);
|
||||
const branches = await git.listRemoteBranches(repoPath);
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const stackEntries = await driver.stack.listStack(repoPath).catch(() => []);
|
||||
const parentByBranch = parentLookupFromStack(stackEntries);
|
||||
const enriched: EnrichedBranchSnapshot[] = [];
|
||||
|
||||
const baseRef = await git.remoteDefaultBaseRef(repoPath);
|
||||
const baseSha = await git.revParse(repoPath, baseRef).catch(() => "");
|
||||
|
||||
for (const branch of branches) {
|
||||
let branchDiffStat: string | null = null;
|
||||
let branchHasUnpushed = false;
|
||||
let branchConflicts = false;
|
||||
|
||||
try {
|
||||
branchDiffStat = await git.diffStatForBranch(repoPath, branch.branchName);
|
||||
} catch (error) {
|
||||
logActorWarning("project-branch-sync", "diffStatForBranch failed", {
|
||||
workspaceId,
|
||||
repoId,
|
||||
branchName: branch.branchName,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
branchDiffStat = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const headSha = await git.revParse(repoPath, `origin/${branch.branchName}`);
|
||||
branchHasUnpushed = Boolean(baseSha && headSha && headSha !== baseSha);
|
||||
} catch (error) {
|
||||
logActorWarning("project-branch-sync", "revParse failed", {
|
||||
workspaceId,
|
||||
repoId,
|
||||
branchName: branch.branchName,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
branchHasUnpushed = false;
|
||||
}
|
||||
|
||||
try {
|
||||
branchConflicts = await git.conflictsWithMain(repoPath, branch.branchName);
|
||||
} catch (error) {
|
||||
logActorWarning("project-branch-sync", "conflictsWithMain failed", {
|
||||
workspaceId,
|
||||
repoId,
|
||||
branchName: branch.branchName,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
branchConflicts = false;
|
||||
}
|
||||
|
||||
enriched.push({
|
||||
branchName: branch.branchName,
|
||||
commitSha: branch.commitSha,
|
||||
parentBranch: parentByBranch.get(branch.branchName) ?? null,
|
||||
trackedInStack: parentByBranch.has(branch.branchName),
|
||||
diffStat: branchDiffStat,
|
||||
hasUnpushed: branchHasUnpushed,
|
||||
conflictsWithMain: branchConflicts,
|
||||
});
|
||||
}
|
||||
|
||||
return enriched;
|
||||
});
|
||||
}
|
||||
|
||||
async function pollBranches(c: { state: ProjectBranchSyncState }): Promise<void> {
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const enrichedItems = await enrichBranches(c.state.workspaceId, c.state.repoId, c.state.repoPath, driver.git);
|
||||
const parent = getProject(c, c.state.workspaceId, c.state.repoId);
|
||||
await parent.applyBranchSyncResult({ items: enrichedItems, at: Date.now() });
|
||||
}
|
||||
|
||||
export const projectBranchSync = actor({
|
||||
queues: {
|
||||
[CONTROL.start]: queue(),
|
||||
[CONTROL.stop]: queue(),
|
||||
[CONTROL.setInterval]: queue(),
|
||||
[CONTROL.force]: queue(),
|
||||
},
|
||||
options: {
|
||||
name: "Project Branch Sync",
|
||||
icon: "code-branch",
|
||||
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
||||
noSleep: true,
|
||||
},
|
||||
createState: (_c, input: ProjectBranchSyncInput): ProjectBranchSyncState => ({
|
||||
workspaceId: input.workspaceId,
|
||||
repoId: input.repoId,
|
||||
repoPath: input.repoPath,
|
||||
intervalMs: input.intervalMs,
|
||||
running: true,
|
||||
}),
|
||||
actions: {
|
||||
async start(c): Promise<void> {
|
||||
const self = selfProjectBranchSync(c);
|
||||
await self.send(CONTROL.start, {}, { wait: true, timeout: 15_000 });
|
||||
},
|
||||
|
||||
async stop(c): Promise<void> {
|
||||
const self = selfProjectBranchSync(c);
|
||||
await self.send(CONTROL.stop, {}, { wait: true, timeout: 15_000 });
|
||||
},
|
||||
|
||||
async setIntervalMs(c, payload: SetIntervalCommand): Promise<void> {
|
||||
const self = selfProjectBranchSync(c);
|
||||
await self.send(CONTROL.setInterval, payload, { wait: true, timeout: 15_000 });
|
||||
},
|
||||
|
||||
async force(c): Promise<void> {
|
||||
const self = selfProjectBranchSync(c);
|
||||
await self.send(CONTROL.force, {}, { wait: true, timeout: 5 * 60_000 });
|
||||
},
|
||||
},
|
||||
run: workflow(async (ctx) => {
|
||||
await runWorkflowPollingLoop<ProjectBranchSyncState>(ctx, {
|
||||
loopName: "project-branch-sync-loop",
|
||||
control: CONTROL,
|
||||
onPoll: async (loopCtx) => {
|
||||
try {
|
||||
await pollBranches(loopCtx);
|
||||
} catch (error) {
|
||||
logActorWarning("project-branch-sync", "poll failed", {
|
||||
error: resolveErrorMessage(error),
|
||||
stack: resolveErrorStack(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +0,0 @@
|
|||
import { defineConfig } from "rivetkit/db/drizzle";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/actors/project/db/drizzle",
|
||||
schema: "./src/actors/project/db/schema.ts",
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
CREATE TABLE `branches` (
|
||||
`branch_name` text PRIMARY KEY NOT NULL,
|
||||
`commit_sha` text NOT NULL,
|
||||
`parent_branch` text,
|
||||
`tracked_in_stack` integer DEFAULT 0 NOT NULL,
|
||||
`diff_stat` text,
|
||||
`has_unpushed` integer DEFAULT 0 NOT NULL,
|
||||
`conflicts_with_main` integer DEFAULT 0 NOT NULL,
|
||||
`first_seen_at` integer,
|
||||
`last_seen_at` integer,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `pr_cache` (
|
||||
`branch_name` text PRIMARY KEY NOT NULL,
|
||||
`pr_number` integer NOT NULL,
|
||||
`state` text NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`pr_url` text,
|
||||
`pr_author` text,
|
||||
`is_draft` integer DEFAULT 0 NOT NULL,
|
||||
`ci_status` text,
|
||||
`review_status` text,
|
||||
`reviewer` text,
|
||||
`fetched_at` integer,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `repo_meta` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`remote_url` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `task_index` (
|
||||
`task_id` text PRIMARY KEY NOT NULL,
|
||||
`branch_name` text,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "6ffd6acb-e737-46ee-a8fe-fcfddcdd6ea9",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"branches": {
|
||||
"name": "branches",
|
||||
"columns": {
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"commit_sha": {
|
||||
"name": "commit_sha",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_branch": {
|
||||
"name": "parent_branch",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tracked_in_stack": {
|
||||
"name": "tracked_in_stack",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"diff_stat": {
|
||||
"name": "diff_stat",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_unpushed": {
|
||||
"name": "has_unpushed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"conflicts_with_main": {
|
||||
"name": "conflicts_with_main",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"first_seen_at": {
|
||||
"name": "first_seen_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_seen_at": {
|
||||
"name": "last_seen_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"pr_cache": {
|
||||
"name": "pr_cache",
|
||||
"columns": {
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_number": {
|
||||
"name": "pr_number",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_url": {
|
||||
"name": "pr_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_author": {
|
||||
"name": "pr_author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_draft": {
|
||||
"name": "is_draft",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"ci_status": {
|
||||
"name": "ci_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"review_status": {
|
||||
"name": "review_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reviewer": {
|
||||
"name": "reviewer",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fetched_at": {
|
||||
"name": "fetched_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repo_meta": {
|
||||
"name": "repo_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"remote_url": {
|
||||
"name": "remote_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"task_index": {
|
||||
"name": "task_index",
|
||||
"columns": {
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
// This file is generated by src/actors/_scripts/generate-actor-migrations.ts.
|
||||
// Source of truth is drizzle-kit output under ./drizzle (meta/_journal.json + *.sql).
|
||||
// Do not hand-edit this file.
|
||||
|
||||
const journal = {
|
||||
entries: [
|
||||
{
|
||||
idx: 0,
|
||||
when: 1773376221848,
|
||||
tag: "0000_useful_la_nuit",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
export default {
|
||||
journal,
|
||||
migrations: {
|
||||
m0000: `CREATE TABLE \`branches\` (
|
||||
\`branch_name\` text PRIMARY KEY NOT NULL,
|
||||
\`commit_sha\` text NOT NULL,
|
||||
\`parent_branch\` text,
|
||||
\`tracked_in_stack\` integer DEFAULT 0 NOT NULL,
|
||||
\`diff_stat\` text,
|
||||
\`has_unpushed\` integer DEFAULT 0 NOT NULL,
|
||||
\`conflicts_with_main\` integer DEFAULT 0 NOT NULL,
|
||||
\`first_seen_at\` integer,
|
||||
\`last_seen_at\` integer,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`repo_meta\` (
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`remote_url\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`task_index\` (
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`branch_name\` text,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
|
||||
// SQLite is per project actor instance (workspaceId+repoId), so no workspaceId/repoId columns needed.
|
||||
|
||||
export const branches = sqliteTable("branches", {
|
||||
branchName: text("branch_name").notNull().primaryKey(),
|
||||
commitSha: text("commit_sha").notNull(),
|
||||
parentBranch: text("parent_branch"),
|
||||
trackedInStack: integer("tracked_in_stack").notNull().default(0),
|
||||
diffStat: text("diff_stat"),
|
||||
hasUnpushed: integer("has_unpushed").notNull().default(0),
|
||||
conflictsWithMain: integer("conflicts_with_main").notNull().default(0),
|
||||
firstSeenAt: integer("first_seen_at"),
|
||||
lastSeenAt: integer("last_seen_at"),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const repoMeta = sqliteTable("repo_meta", {
|
||||
id: integer("id").primaryKey(),
|
||||
remoteUrl: text("remote_url").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const taskIndex = sqliteTable("task_index", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
branchName: text("branch_name"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const repoActionJobs = sqliteTable("repo_action_jobs", {
|
||||
jobId: text("job_id").notNull().primaryKey(),
|
||||
action: text("action").notNull(),
|
||||
branchName: text("branch_name"),
|
||||
parentBranch: text("parent_branch"),
|
||||
status: text("status").notNull(),
|
||||
message: text("message").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
completedAt: integer("completed_at"),
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { actor, queue } from "rivetkit";
|
||||
import { workflow } from "rivetkit/workflow";
|
||||
import { projectDb } from "./db/db.js";
|
||||
import { PROJECT_QUEUE_NAMES, projectActions, runProjectWorkflow } from "./actions.js";
|
||||
|
||||
export interface ProjectInput {
|
||||
workspaceId: string;
|
||||
repoId: string;
|
||||
remoteUrl: string;
|
||||
}
|
||||
|
||||
export const project = actor({
|
||||
db: projectDb,
|
||||
queues: Object.fromEntries(PROJECT_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||
options: {
|
||||
name: "Project",
|
||||
icon: "folder",
|
||||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, input: ProjectInput) => ({
|
||||
workspaceId: input.workspaceId,
|
||||
repoId: input.repoId,
|
||||
remoteUrl: input.remoteUrl,
|
||||
localPath: null as string | null,
|
||||
syncActorsStarted: false,
|
||||
taskIndexHydrated: false,
|
||||
}),
|
||||
actions: projectActions,
|
||||
run: workflow(runProjectWorkflow),
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
export interface StackEntry {
|
||||
branchName: string;
|
||||
parentBranch: string | null;
|
||||
}
|
||||
|
||||
export interface OrderedBranchRow {
|
||||
branchName: string;
|
||||
parentBranch: string | null;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export function normalizeParentBranch(branchName: string, parentBranch: string | null | undefined): string | null {
|
||||
const parent = parentBranch?.trim() || null;
|
||||
if (!parent || parent === branchName) {
|
||||
return null;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
export function parentLookupFromStack(entries: StackEntry[]): Map<string, string | null> {
|
||||
const lookup = new Map<string, string | null>();
|
||||
for (const entry of entries) {
|
||||
const branchName = entry.branchName.trim();
|
||||
if (!branchName) {
|
||||
continue;
|
||||
}
|
||||
lookup.set(branchName, normalizeParentBranch(branchName, entry.parentBranch));
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
export function sortBranchesForOverview(rows: OrderedBranchRow[]): OrderedBranchRow[] {
|
||||
const byName = new Map(rows.map((row) => [row.branchName, row]));
|
||||
const depthMemo = new Map<string, number>();
|
||||
const computing = new Set<string>();
|
||||
|
||||
const depthFor = (branchName: string): number => {
|
||||
const cached = depthMemo.get(branchName);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
if (computing.has(branchName)) {
|
||||
return 999;
|
||||
}
|
||||
|
||||
computing.add(branchName);
|
||||
const row = byName.get(branchName);
|
||||
const parent = row?.parentBranch;
|
||||
let depth = 0;
|
||||
if (parent && parent !== branchName && byName.has(parent)) {
|
||||
depth = Math.min(998, depthFor(parent) + 1);
|
||||
}
|
||||
computing.delete(branchName);
|
||||
depthMemo.set(branchName, depth);
|
||||
return depth;
|
||||
};
|
||||
|
||||
return [...rows].sort((a, b) => {
|
||||
const da = depthFor(a.branchName);
|
||||
const db = depthFor(b.branchName);
|
||||
if (da !== db) {
|
||||
return da - db;
|
||||
}
|
||||
if (a.updatedAt !== b.updatedAt) {
|
||||
return b.updatedAt - a.updatedAt;
|
||||
}
|
||||
return a.branchName.localeCompare(b.branchName);
|
||||
});
|
||||
}
|
||||
557
foundry/packages/backend/src/actors/repository/actions.ts
Normal file
557
foundry/packages/backend/src/actors/repository/actions.ts
Normal file
|
|
@ -0,0 +1,557 @@
|
|||
// @ts-nocheck
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { and, desc, eq, isNotNull, ne } from "drizzle-orm";
|
||||
import { Loop } from "rivetkit/workflow";
|
||||
import type { AgentType, RepoOverview, SandboxProviderId, TaskRecord, TaskSummary } from "@sandbox-agent/foundry-shared";
|
||||
import { getGithubData, getOrCreateHistory, getOrCreateTask, getTask, selfRepository } from "../handles.js";
|
||||
import { deriveFallbackTitle, resolveCreateFlowDecision } from "../../services/create-flow.js";
|
||||
import { expectQueueResponse } from "../../services/queue.js";
|
||||
import { isActorNotFoundError, logActorWarning, resolveErrorMessage } from "../logging.js";
|
||||
import { repoMeta, taskIndex } from "./db/schema.js";
|
||||
|
||||
interface CreateTaskCommand {
|
||||
task: string;
|
||||
sandboxProviderId: SandboxProviderId;
|
||||
agentType: AgentType | null;
|
||||
explicitTitle: string | null;
|
||||
explicitBranchName: string | null;
|
||||
initialPrompt: string | null;
|
||||
onBranch: string | null;
|
||||
}
|
||||
|
||||
interface RegisterTaskBranchCommand {
|
||||
taskId: string;
|
||||
branchName: string;
|
||||
requireExistingRemote?: boolean;
|
||||
}
|
||||
|
||||
interface ListTaskSummariesCommand {
|
||||
includeArchived?: boolean;
|
||||
}
|
||||
|
||||
interface GetTaskEnrichedCommand {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
interface GetPullRequestForBranchCommand {
|
||||
branchName: string;
|
||||
}
|
||||
|
||||
const REPOSITORY_QUEUE_NAMES = ["repository.command.createTask", "repository.command.registerTaskBranch"] as const;
|
||||
|
||||
type RepositoryQueueName = (typeof REPOSITORY_QUEUE_NAMES)[number];
|
||||
|
||||
export { REPOSITORY_QUEUE_NAMES };
|
||||
|
||||
export function repositoryWorkflowQueueName(name: RepositoryQueueName): RepositoryQueueName {
|
||||
return name;
|
||||
}
|
||||
|
||||
function isStaleTaskReferenceError(error: unknown): boolean {
|
||||
const message = resolveErrorMessage(error);
|
||||
return isActorNotFoundError(error) || message.startsWith("Task not found:");
|
||||
}
|
||||
|
||||
async function persistRemoteUrl(c: any, remoteUrl: string): Promise<void> {
|
||||
c.state.remoteUrl = remoteUrl;
|
||||
await c.db
|
||||
.insert(repoMeta)
|
||||
.values({
|
||||
id: 1,
|
||||
remoteUrl,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: repoMeta.id,
|
||||
set: {
|
||||
remoteUrl,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
async function deleteStaleTaskIndexRow(c: any, taskId: string): Promise<void> {
|
||||
try {
|
||||
await c.db.delete(taskIndex).where(eq(taskIndex.taskId, taskId)).run();
|
||||
} catch {
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
}
|
||||
|
||||
async function reinsertTaskIndexRow(c: any, taskId: string, branchName: string | null, updatedAt: number): Promise<void> {
|
||||
const now = Date.now();
|
||||
await c.db
|
||||
.insert(taskIndex)
|
||||
.values({
|
||||
taskId,
|
||||
branchName,
|
||||
createdAt: updatedAt || now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: taskIndex.taskId,
|
||||
set: {
|
||||
branchName,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
async function listKnownTaskBranches(c: any): Promise<string[]> {
|
||||
const rows = await c.db.select({ branchName: taskIndex.branchName }).from(taskIndex).where(isNotNull(taskIndex.branchName)).all();
|
||||
return rows.map((row) => row.branchName).filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
||||
}
|
||||
|
||||
async function resolveGitHubRepository(c: any) {
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
return await githubData.getRepository({ repoId: c.state.repoId }).catch(() => null);
|
||||
}
|
||||
|
||||
async function listGitHubBranches(c: any): Promise<Array<{ branchName: string; commitSha: string }>> {
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
return await githubData.listBranchesForRepository({ repoId: c.state.repoId }).catch(() => []);
|
||||
}
|
||||
|
||||
async function enrichTaskRecord(c: any, record: TaskRecord): Promise<TaskRecord> {
|
||||
const branchName = record.branchName?.trim() || null;
|
||||
if (!branchName) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const pr =
|
||||
branchName != null
|
||||
? await getGithubData(c, c.state.organizationId)
|
||||
.listPullRequestsForRepository({ repoId: c.state.repoId })
|
||||
.then((rows: any[]) => rows.find((row) => row.headRefName === branchName) ?? null)
|
||||
.catch(() => null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...record,
|
||||
prUrl: pr?.url ?? null,
|
||||
prAuthor: pr?.authorLogin ?? null,
|
||||
ciStatus: null,
|
||||
reviewStatus: null,
|
||||
reviewer: pr?.authorLogin ?? null,
|
||||
diffStat: record.diffStat ?? null,
|
||||
hasUnpushed: record.hasUnpushed ?? null,
|
||||
conflictsWithMain: record.conflictsWithMain ?? null,
|
||||
parentBranch: record.parentBranch ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise<TaskRecord> {
|
||||
const organizationId = c.state.organizationId;
|
||||
const repoId = c.state.repoId;
|
||||
const repoRemote = c.state.remoteUrl;
|
||||
const onBranch = cmd.onBranch?.trim() || null;
|
||||
const taskId = randomUUID();
|
||||
let initialBranchName: string | null = null;
|
||||
let initialTitle: string | null = null;
|
||||
|
||||
await persistRemoteUrl(c, repoRemote);
|
||||
|
||||
if (onBranch) {
|
||||
initialBranchName = onBranch;
|
||||
initialTitle = deriveFallbackTitle(cmd.task, cmd.explicitTitle ?? undefined);
|
||||
|
||||
await registerTaskBranchMutation(c, {
|
||||
taskId,
|
||||
branchName: onBranch,
|
||||
requireExistingRemote: true,
|
||||
});
|
||||
} else {
|
||||
const reservedBranches = await listKnownTaskBranches(c);
|
||||
const resolved = resolveCreateFlowDecision({
|
||||
task: cmd.task,
|
||||
explicitTitle: cmd.explicitTitle ?? undefined,
|
||||
explicitBranchName: cmd.explicitBranchName ?? undefined,
|
||||
localBranches: [],
|
||||
taskBranches: reservedBranches,
|
||||
});
|
||||
|
||||
initialBranchName = resolved.branchName;
|
||||
initialTitle = resolved.title;
|
||||
|
||||
const now = Date.now();
|
||||
await c.db
|
||||
.insert(taskIndex)
|
||||
.values({
|
||||
taskId,
|
||||
branchName: resolved.branchName,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
}
|
||||
|
||||
let taskHandle: Awaited<ReturnType<typeof getOrCreateTask>>;
|
||||
try {
|
||||
taskHandle = await getOrCreateTask(c, organizationId, repoId, taskId, {
|
||||
organizationId,
|
||||
repoId,
|
||||
taskId,
|
||||
repoRemote,
|
||||
branchName: initialBranchName,
|
||||
title: initialTitle,
|
||||
task: cmd.task,
|
||||
sandboxProviderId: cmd.sandboxProviderId,
|
||||
agentType: cmd.agentType,
|
||||
explicitTitle: null,
|
||||
explicitBranchName: null,
|
||||
initialPrompt: cmd.initialPrompt,
|
||||
});
|
||||
} catch (error) {
|
||||
if (initialBranchName) {
|
||||
await deleteStaleTaskIndexRow(c, taskId);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const created = await taskHandle.initialize({ sandboxProviderId: cmd.sandboxProviderId });
|
||||
|
||||
const history = await getOrCreateHistory(c, organizationId, repoId);
|
||||
await history.append({
|
||||
kind: "task.created",
|
||||
taskId,
|
||||
payload: {
|
||||
repoId,
|
||||
sandboxProviderId: cmd.sandboxProviderId,
|
||||
},
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async function registerTaskBranchMutation(c: any, cmd: RegisterTaskBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
||||
const branchName = cmd.branchName.trim();
|
||||
if (!branchName) {
|
||||
throw new Error("branchName is required");
|
||||
}
|
||||
|
||||
await persistRemoteUrl(c, c.state.remoteUrl);
|
||||
|
||||
const existingOwner = await c.db
|
||||
.select({ taskId: taskIndex.taskId })
|
||||
.from(taskIndex)
|
||||
.where(and(eq(taskIndex.branchName, branchName), ne(taskIndex.taskId, cmd.taskId)))
|
||||
.get();
|
||||
|
||||
if (existingOwner) {
|
||||
let ownerMissing = false;
|
||||
try {
|
||||
await getTask(c, c.state.organizationId, c.state.repoId, existingOwner.taskId).get();
|
||||
} catch (error) {
|
||||
if (isStaleTaskReferenceError(error)) {
|
||||
ownerMissing = true;
|
||||
await deleteStaleTaskIndexRow(c, existingOwner.taskId);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (!ownerMissing) {
|
||||
throw new Error(`branch is already assigned to a different task: ${branchName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const branches = await listGitHubBranches(c);
|
||||
const branchMatch = branches.find((branch) => branch.branchName === branchName) ?? null;
|
||||
if (cmd.requireExistingRemote && !branchMatch) {
|
||||
throw new Error(`Remote branch not found: ${branchName}`);
|
||||
}
|
||||
|
||||
const repository = await resolveGitHubRepository(c);
|
||||
const defaultBranch = repository?.defaultBranch ?? "main";
|
||||
const headSha = branchMatch?.commitSha ?? branches.find((branch) => branch.branchName === defaultBranch)?.commitSha ?? "";
|
||||
|
||||
const now = Date.now();
|
||||
await c.db
|
||||
.insert(taskIndex)
|
||||
.values({
|
||||
taskId: cmd.taskId,
|
||||
branchName,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: taskIndex.taskId,
|
||||
set: {
|
||||
branchName,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
|
||||
return { branchName, headSha };
|
||||
}
|
||||
|
||||
async function listTaskSummaries(c: any, includeArchived = false): Promise<TaskSummary[]> {
|
||||
const taskRows = await c.db.select({ taskId: taskIndex.taskId }).from(taskIndex).orderBy(desc(taskIndex.updatedAt)).all();
|
||||
const records: TaskSummary[] = [];
|
||||
|
||||
for (const row of taskRows) {
|
||||
try {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, row.taskId).get();
|
||||
if (!includeArchived && record.status === "archived") {
|
||||
continue;
|
||||
}
|
||||
records.push({
|
||||
organizationId: record.organizationId,
|
||||
repoId: record.repoId,
|
||||
taskId: record.taskId,
|
||||
branchName: record.branchName,
|
||||
title: record.title,
|
||||
status: record.status,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isStaleTaskReferenceError(error)) {
|
||||
await deleteStaleTaskIndexRow(c, row.taskId);
|
||||
continue;
|
||||
}
|
||||
logActorWarning("repository", "failed loading task summary row", {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: c.state.repoId,
|
||||
taskId: row.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
records.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
return records;
|
||||
}
|
||||
|
||||
function sortOverviewBranches(
|
||||
branches: Array<{
|
||||
branchName: string;
|
||||
commitSha: string;
|
||||
taskId: string | null;
|
||||
taskTitle: string | null;
|
||||
taskStatus: TaskRecord["status"] | null;
|
||||
prNumber: number | null;
|
||||
prState: string | null;
|
||||
prUrl: string | null;
|
||||
ciStatus: string | null;
|
||||
reviewStatus: string | null;
|
||||
reviewer: string | null;
|
||||
updatedAt: number;
|
||||
}>,
|
||||
defaultBranch: string | null,
|
||||
) {
|
||||
return [...branches].sort((left, right) => {
|
||||
if (defaultBranch) {
|
||||
if (left.branchName === defaultBranch && right.branchName !== defaultBranch) return -1;
|
||||
if (right.branchName === defaultBranch && left.branchName !== defaultBranch) return 1;
|
||||
}
|
||||
if (Boolean(left.taskId) !== Boolean(right.taskId)) {
|
||||
return left.taskId ? -1 : 1;
|
||||
}
|
||||
if (left.updatedAt !== right.updatedAt) {
|
||||
return right.updatedAt - left.updatedAt;
|
||||
}
|
||||
return left.branchName.localeCompare(right.branchName);
|
||||
});
|
||||
}
|
||||
|
||||
export async function runRepositoryWorkflow(ctx: any): Promise<void> {
|
||||
await ctx.loop("repository-command-loop", async (loopCtx: any) => {
|
||||
const msg = await loopCtx.queue.next("next-repository-command", {
|
||||
names: [...REPOSITORY_QUEUE_NAMES],
|
||||
completable: true,
|
||||
});
|
||||
if (!msg) {
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
try {
|
||||
if (msg.name === "repository.command.createTask") {
|
||||
const result = await loopCtx.step({
|
||||
name: "repository-create-task",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => createTaskMutation(loopCtx, msg.body as CreateTaskCommand),
|
||||
});
|
||||
await msg.complete(result);
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "repository.command.registerTaskBranch") {
|
||||
const result = await loopCtx.step({
|
||||
name: "repository-register-task-branch",
|
||||
timeout: 60_000,
|
||||
run: async () => registerTaskBranchMutation(loopCtx, msg.body as RegisterTaskBranchCommand),
|
||||
});
|
||||
await msg.complete(result);
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = resolveErrorMessage(error);
|
||||
logActorWarning("repository", "repository workflow command failed", {
|
||||
queueName: msg.name,
|
||||
error: message,
|
||||
});
|
||||
await msg.complete({ error: message }).catch(() => {});
|
||||
}
|
||||
|
||||
return Loop.continue(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
export const repositoryActions = {
|
||||
async createTask(c: any, cmd: CreateTaskCommand): Promise<TaskRecord> {
|
||||
const self = selfRepository(c);
|
||||
return expectQueueResponse<TaskRecord>(
|
||||
await self.send(repositoryWorkflowQueueName("repository.command.createTask"), cmd, {
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async listReservedBranches(c: any): Promise<string[]> {
|
||||
return await listKnownTaskBranches(c);
|
||||
},
|
||||
|
||||
async registerTaskBranch(c: any, cmd: RegisterTaskBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
||||
const self = selfRepository(c);
|
||||
return expectQueueResponse<{ branchName: string; headSha: string }>(
|
||||
await self.send(repositoryWorkflowQueueName("repository.command.registerTaskBranch"), cmd, {
|
||||
wait: true,
|
||||
timeout: 10_000,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async listTaskSummaries(c: any, cmd?: ListTaskSummariesCommand): Promise<TaskSummary[]> {
|
||||
return await listTaskSummaries(c, cmd?.includeArchived === true);
|
||||
},
|
||||
|
||||
async getTaskEnriched(c: any, cmd: GetTaskEnrichedCommand): Promise<TaskRecord> {
|
||||
const row = await c.db.select({ taskId: taskIndex.taskId }).from(taskIndex).where(eq(taskIndex.taskId, cmd.taskId)).get();
|
||||
if (!row) {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, cmd.taskId).get();
|
||||
await reinsertTaskIndexRow(c, cmd.taskId, record.branchName ?? null, record.updatedAt ?? Date.now());
|
||||
return await enrichTaskRecord(c, record);
|
||||
}
|
||||
|
||||
try {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, cmd.taskId).get();
|
||||
return await enrichTaskRecord(c, record);
|
||||
} catch (error) {
|
||||
if (isStaleTaskReferenceError(error)) {
|
||||
await deleteStaleTaskIndexRow(c, cmd.taskId);
|
||||
throw new Error(`Unknown task in repo ${c.state.repoId}: ${cmd.taskId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getRepositoryMetadata(c: any): Promise<{ defaultBranch: string | null; fullName: string | null; remoteUrl: string }> {
|
||||
const repository = await resolveGitHubRepository(c);
|
||||
return {
|
||||
defaultBranch: repository?.defaultBranch ?? null,
|
||||
fullName: repository?.fullName ?? null,
|
||||
remoteUrl: c.state.remoteUrl,
|
||||
};
|
||||
},
|
||||
|
||||
async getRepoOverview(c: any): Promise<RepoOverview> {
|
||||
await persistRemoteUrl(c, c.state.remoteUrl);
|
||||
|
||||
const now = Date.now();
|
||||
const repository = await resolveGitHubRepository(c);
|
||||
const githubBranches = await listGitHubBranches(c).catch(() => []);
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
const prRows = await githubData.listPullRequestsForRepository({ repoId: c.state.repoId }).catch(() => []);
|
||||
const prByBranch = new Map(prRows.map((row) => [row.headRefName, row]));
|
||||
|
||||
const taskRows = await c.db
|
||||
.select({
|
||||
taskId: taskIndex.taskId,
|
||||
branchName: taskIndex.branchName,
|
||||
updatedAt: taskIndex.updatedAt,
|
||||
})
|
||||
.from(taskIndex)
|
||||
.all();
|
||||
|
||||
const taskMetaByBranch = new Map<string, { taskId: string; title: string | null; status: TaskRecord["status"] | null; updatedAt: number }>();
|
||||
for (const row of taskRows) {
|
||||
if (!row.branchName) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const record = await getTask(c, c.state.organizationId, c.state.repoId, row.taskId).get();
|
||||
taskMetaByBranch.set(row.branchName, {
|
||||
taskId: row.taskId,
|
||||
title: record.title ?? null,
|
||||
status: record.status,
|
||||
updatedAt: record.updatedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isStaleTaskReferenceError(error)) {
|
||||
await deleteStaleTaskIndexRow(c, row.taskId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const branchMap = new Map<string, { branchName: string; commitSha: string }>();
|
||||
for (const branch of githubBranches) {
|
||||
branchMap.set(branch.branchName, branch);
|
||||
}
|
||||
for (const branchName of taskMetaByBranch.keys()) {
|
||||
if (!branchMap.has(branchName)) {
|
||||
branchMap.set(branchName, { branchName, commitSha: "" });
|
||||
}
|
||||
}
|
||||
if (repository?.defaultBranch && !branchMap.has(repository.defaultBranch)) {
|
||||
branchMap.set(repository.defaultBranch, { branchName: repository.defaultBranch, commitSha: "" });
|
||||
}
|
||||
|
||||
const branches = sortOverviewBranches(
|
||||
[...branchMap.values()].map((branch) => {
|
||||
const taskMeta = taskMetaByBranch.get(branch.branchName);
|
||||
const pr = prByBranch.get(branch.branchName);
|
||||
return {
|
||||
branchName: branch.branchName,
|
||||
commitSha: branch.commitSha,
|
||||
taskId: taskMeta?.taskId ?? null,
|
||||
taskTitle: taskMeta?.title ?? null,
|
||||
taskStatus: taskMeta?.status ?? null,
|
||||
prNumber: pr?.number ?? null,
|
||||
prState: pr?.state ?? null,
|
||||
prUrl: pr?.url ?? null,
|
||||
ciStatus: null,
|
||||
reviewStatus: null,
|
||||
reviewer: pr?.authorLogin ?? null,
|
||||
updatedAt: Math.max(taskMeta?.updatedAt ?? 0, pr?.updatedAtMs ?? 0, now),
|
||||
};
|
||||
}),
|
||||
repository?.defaultBranch ?? null,
|
||||
);
|
||||
|
||||
return {
|
||||
organizationId: c.state.organizationId,
|
||||
repoId: c.state.repoId,
|
||||
remoteUrl: c.state.remoteUrl,
|
||||
baseRef: repository?.defaultBranch ?? null,
|
||||
fetchedAt: now,
|
||||
branches,
|
||||
};
|
||||
},
|
||||
|
||||
async getPullRequestForBranch(c: any, cmd: GetPullRequestForBranchCommand): Promise<{ number: number; status: "draft" | "ready" } | null> {
|
||||
const branchName = cmd.branchName?.trim();
|
||||
if (!branchName) {
|
||||
return null;
|
||||
}
|
||||
const githubData = getGithubData(c, c.state.organizationId);
|
||||
return await githubData.getPullRequestForBranch({
|
||||
repoId: c.state.repoId,
|
||||
branchName,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -2,4 +2,4 @@ import { db } from "rivetkit/db/drizzle";
|
|||
import * as schema from "./schema.js";
|
||||
import migrations from "./migrations.js";
|
||||
|
||||
export const workspaceDb = db({ schema, migrations });
|
||||
export const repositoryDb = db({ schema, migrations });
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { defineConfig } from "rivetkit/db/drizzle";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/actors/repository/db/drizzle",
|
||||
schema: "./src/actors/repository/db/schema.ts",
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE `repo_meta` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`remote_url` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `task_index` (
|
||||
`task_id` text PRIMARY KEY NOT NULL,
|
||||
`branch_name` text,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "6ffd6acb-e737-46ee-a8fe-fcfddcdd6ea9",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"repo_meta": {
|
||||
"name": "repo_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"remote_url": {
|
||||
"name": "remote_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"task_index": {
|
||||
"name": "task_index",
|
||||
"columns": {
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// This file is generated by src/actors/_scripts/generate-actor-migrations.ts.
|
||||
// Source of truth is drizzle-kit output under ./drizzle (meta/_journal.json + *.sql).
|
||||
// Do not hand-edit this file.
|
||||
|
||||
const journal = {
|
||||
entries: [
|
||||
{
|
||||
idx: 0,
|
||||
when: 1773376221848,
|
||||
tag: "0000_useful_la_nuit",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1778900000000,
|
||||
tag: "0001_remove_local_git_state",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
export default {
|
||||
journal,
|
||||
migrations: {
|
||||
m0000: `CREATE TABLE \`repo_meta\` (
|
||||
\t\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\t\`remote_url\` text NOT NULL,
|
||||
\t\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`task_index\` (
|
||||
\t\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\t\`branch_name\` text,
|
||||
\t\`created_at\` integer NOT NULL,
|
||||
\t\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `DROP TABLE IF EXISTS \`branches\`;
|
||||
--> statement-breakpoint
|
||||
DROP TABLE IF EXISTS \`repo_action_jobs\`;
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
16
foundry/packages/backend/src/actors/repository/db/schema.ts
Normal file
16
foundry/packages/backend/src/actors/repository/db/schema.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
|
||||
// SQLite is per repository actor instance (organizationId+repoId).
|
||||
|
||||
export const repoMeta = sqliteTable("repo_meta", {
|
||||
id: integer("id").primaryKey(),
|
||||
remoteUrl: text("remote_url").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const taskIndex = sqliteTable("task_index", {
|
||||
taskId: text("task_id").notNull().primaryKey(),
|
||||
branchName: text("branch_name"),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
27
foundry/packages/backend/src/actors/repository/index.ts
Normal file
27
foundry/packages/backend/src/actors/repository/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { actor, queue } from "rivetkit";
|
||||
import { workflow } from "rivetkit/workflow";
|
||||
import { repositoryDb } from "./db/db.js";
|
||||
import { REPOSITORY_QUEUE_NAMES, repositoryActions, runRepositoryWorkflow } from "./actions.js";
|
||||
|
||||
export interface RepositoryInput {
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
remoteUrl: string;
|
||||
}
|
||||
|
||||
export const repository = actor({
|
||||
db: repositoryDb,
|
||||
queues: Object.fromEntries(REPOSITORY_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||
options: {
|
||||
name: "Repository",
|
||||
icon: "folder",
|
||||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, input: RepositoryInput) => ({
|
||||
organizationId: input.organizationId,
|
||||
repoId: input.repoId,
|
||||
remoteUrl: input.remoteUrl,
|
||||
}),
|
||||
actions: repositoryActions,
|
||||
run: workflow(runRepositoryWorkflow),
|
||||
});
|
||||
|
|
@ -4,21 +4,21 @@ import { existsSync } from "node:fs";
|
|||
import Dockerode from "dockerode";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { workspaceKey } from "../keys.js";
|
||||
import { organizationKey } from "../keys.js";
|
||||
import { resolveSandboxProviderId } from "../../sandbox-config.js";
|
||||
|
||||
const SANDBOX_REPO_CWD = "/home/sandbox/workspace/repo";
|
||||
const SANDBOX_REPO_CWD = "/home/sandbox/organization/repo";
|
||||
const DEFAULT_LOCAL_SANDBOX_IMAGE = "rivetdev/sandbox-agent:full";
|
||||
const DEFAULT_LOCAL_SANDBOX_PORT = 2468;
|
||||
const dockerClient = new Dockerode({ socketPath: "/var/run/docker.sock" });
|
||||
|
||||
function parseTaskSandboxKey(key: readonly string[]): { workspaceId: string; taskId: string } {
|
||||
if (key.length !== 4 || key[0] !== "ws" || key[2] !== "sandbox") {
|
||||
function parseTaskSandboxKey(key: readonly string[]): { organizationId: string; taskId: string } {
|
||||
if (key.length !== 4 || key[0] !== "org" || key[2] !== "sandbox") {
|
||||
throw new Error(`Invalid task sandbox key: ${JSON.stringify(key)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: key[1]!,
|
||||
organizationId: key[1]!,
|
||||
taskId: key[3]!,
|
||||
};
|
||||
}
|
||||
|
|
@ -191,24 +191,24 @@ function sanitizeActorResult(value: unknown, seen = new WeakSet<object>()): unkn
|
|||
const baseTaskSandbox = sandboxActor({
|
||||
createProvider: async (c) => {
|
||||
const { config } = getActorRuntimeContext();
|
||||
const { workspaceId, taskId } = parseTaskSandboxKey(c.key);
|
||||
const workspace = await c.client().workspace.getOrCreate(workspaceKey(workspaceId), {
|
||||
createWithInput: workspaceId,
|
||||
const { organizationId, taskId } = parseTaskSandboxKey(c.key);
|
||||
const organization = await c.client().organization.getOrCreate(organizationKey(organizationId), {
|
||||
createWithInput: organizationId,
|
||||
});
|
||||
const task = await workspace.getTask({ workspaceId, taskId });
|
||||
const providerId = resolveSandboxProviderId(config, task.providerId);
|
||||
const task = await organization.getTask({ organizationId, taskId });
|
||||
const sandboxProviderId = resolveSandboxProviderId(config, task.sandboxProviderId);
|
||||
|
||||
if (providerId === "e2b") {
|
||||
if (sandboxProviderId === "e2b") {
|
||||
return e2b({
|
||||
create: () => ({
|
||||
template: config.providers.e2b.template ?? "sandbox-agent-full-0.3.x",
|
||||
template: config.sandboxProviders.e2b.template ?? "sandbox-agent-full-0.3.x",
|
||||
envs: sandboxEnvObject(),
|
||||
}),
|
||||
installAgents: ["claude", "codex"],
|
||||
});
|
||||
}
|
||||
|
||||
return createLocalSandboxProvider(config.providers.local.image ?? process.env.HF_LOCAL_SANDBOX_IMAGE ?? DEFAULT_LOCAL_SANDBOX_IMAGE);
|
||||
return createLocalSandboxProvider(config.sandboxProviders.local.image ?? process.env.HF_LOCAL_SANDBOX_IMAGE ?? DEFAULT_LOCAL_SANDBOX_IMAGE);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -236,23 +236,23 @@ async function providerForConnection(c: any): Promise<any | null> {
|
|||
const providerFactory = baseTaskSandbox.config.actions as Record<string, unknown>;
|
||||
void providerFactory;
|
||||
const { config } = getActorRuntimeContext();
|
||||
const { workspaceId, taskId } = parseTaskSandboxKey(c.key);
|
||||
const workspace = await c.client().workspace.getOrCreate(workspaceKey(workspaceId), {
|
||||
createWithInput: workspaceId,
|
||||
const { organizationId, taskId } = parseTaskSandboxKey(c.key);
|
||||
const organization = await c.client().organization.getOrCreate(organizationKey(organizationId), {
|
||||
createWithInput: organizationId,
|
||||
});
|
||||
const task = await workspace.getTask({ workspaceId, taskId });
|
||||
const providerId = resolveSandboxProviderId(config, task.providerId);
|
||||
const task = await organization.getTask({ organizationId, taskId });
|
||||
const sandboxProviderId = resolveSandboxProviderId(config, task.sandboxProviderId);
|
||||
|
||||
const provider =
|
||||
providerId === "e2b"
|
||||
sandboxProviderId === "e2b"
|
||||
? e2b({
|
||||
create: () => ({
|
||||
template: config.providers.e2b.template ?? "sandbox-agent-full-0.3.x",
|
||||
template: config.sandboxProviders.e2b.template ?? "sandbox-agent-full-0.3.x",
|
||||
envs: sandboxEnvObject(),
|
||||
}),
|
||||
installAgents: ["claude", "codex"],
|
||||
})
|
||||
: createLocalSandboxProvider(config.providers.local.image ?? process.env.HF_LOCAL_SANDBOX_IMAGE ?? DEFAULT_LOCAL_SANDBOX_IMAGE);
|
||||
: createLocalSandboxProvider(config.sandboxProviders.local.image ?? process.env.HF_LOCAL_SANDBOX_IMAGE ?? DEFAULT_LOCAL_SANDBOX_IMAGE);
|
||||
|
||||
c.vars.provider = provider;
|
||||
return provider;
|
||||
|
|
@ -360,31 +360,31 @@ export const taskSandbox = actor({
|
|||
}
|
||||
},
|
||||
|
||||
async providerState(c: any): Promise<{ providerId: "e2b" | "local"; sandboxId: string; state: string; at: number }> {
|
||||
async providerState(c: any): Promise<{ sandboxProviderId: "e2b" | "local"; sandboxId: string; state: string; at: number }> {
|
||||
const { config } = getActorRuntimeContext();
|
||||
const { taskId } = parseTaskSandboxKey(c.key);
|
||||
const at = Date.now();
|
||||
const providerId = resolveSandboxProviderId(config, c.state.providerName === "e2b" ? "e2b" : c.state.providerName === "docker" ? "local" : null);
|
||||
const sandboxProviderId = resolveSandboxProviderId(config, c.state.providerName === "e2b" ? "e2b" : c.state.providerName === "docker" ? "local" : null);
|
||||
|
||||
if (c.state.sandboxDestroyed) {
|
||||
return { providerId, sandboxId: taskId, state: "destroyed", at };
|
||||
return { sandboxProviderId, sandboxId: taskId, state: "destroyed", at };
|
||||
}
|
||||
|
||||
if (!c.state.sandboxId) {
|
||||
return { providerId, sandboxId: taskId, state: "pending", at };
|
||||
return { sandboxProviderId, sandboxId: taskId, state: "pending", at };
|
||||
}
|
||||
|
||||
try {
|
||||
const health = await baseActions.getHealth(c);
|
||||
return {
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
sandboxId: taskId,
|
||||
state: health.status === "ok" ? "running" : "degraded",
|
||||
at,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
sandboxId: taskId,
|
||||
state: "error",
|
||||
at,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ const journal = {
|
|||
tag: "0000_charming_maestro",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1773810000000,
|
||||
tag: "0001_sandbox_provider_columns",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -63,9 +69,13 @@ CREATE TABLE \`task_workbench_sessions\` (
|
|||
\`created\` integer DEFAULT 1 NOT NULL,
|
||||
\`closed\` integer DEFAULT 0 NOT NULL,
|
||||
\`thinking_since_ms\` integer,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `ALTER TABLE \`task\` RENAME COLUMN \`provider_id\` TO \`sandbox_provider_id\`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE \`task_sandboxes\` RENAME COLUMN \`provider_id\` TO \`sandbox_provider_id\`;
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const task = sqliteTable(
|
|||
branchName: text("branch_name"),
|
||||
title: text("title"),
|
||||
task: text("task").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
sandboxProviderId: text("sandbox_provider_id").notNull(),
|
||||
status: text("status").notNull(),
|
||||
agentType: text("agent_type").default("claude"),
|
||||
prSubmitted: integer("pr_submitted").default(0),
|
||||
|
|
@ -39,7 +39,7 @@ export const taskRuntime = sqliteTable(
|
|||
|
||||
export const taskSandboxes = sqliteTable("task_sandboxes", {
|
||||
sandboxId: text("sandbox_id").notNull().primaryKey(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
sandboxProviderId: text("sandbox_provider_id").notNull(),
|
||||
sandboxActorId: text("sandbox_actor_id"),
|
||||
switchTarget: text("switch_target").notNull(),
|
||||
cwd: text("cwd"),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
TaskWorkbenchSetSessionUnreadInput,
|
||||
TaskWorkbenchSendMessageInput,
|
||||
TaskWorkbenchUpdateDraftInput,
|
||||
ProviderId,
|
||||
SandboxProviderId,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { expectQueueResponse } from "../../services/queue.js";
|
||||
import { selfTask } from "../handles.js";
|
||||
|
|
@ -37,15 +37,14 @@ import {
|
|||
import { TASK_QUEUE_NAMES, taskWorkflowQueueName, runTaskWorkflow } from "./workflow/index.js";
|
||||
|
||||
export interface TaskInput {
|
||||
workspaceId: string;
|
||||
organizationId: string;
|
||||
repoId: string;
|
||||
taskId: string;
|
||||
repoRemote: string;
|
||||
repoLocalPath?: string;
|
||||
branchName: string | null;
|
||||
title: string | null;
|
||||
task: string;
|
||||
providerId: ProviderId;
|
||||
sandboxProviderId: SandboxProviderId;
|
||||
agentType: AgentType | null;
|
||||
explicitTitle: string | null;
|
||||
explicitBranchName: string | null;
|
||||
|
|
@ -53,15 +52,15 @@ export interface TaskInput {
|
|||
}
|
||||
|
||||
interface InitializeCommand {
|
||||
providerId?: ProviderId;
|
||||
sandboxProviderId?: SandboxProviderId;
|
||||
}
|
||||
|
||||
interface TaskActionCommand {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
interface TaskTabCommand {
|
||||
tabId: string;
|
||||
interface TaskSessionCommand {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
interface TaskStatusSyncCommand {
|
||||
|
|
@ -101,14 +100,15 @@ interface TaskWorkbenchSendMessageCommand {
|
|||
attachments: Array<any>;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchSendMessageActionInput extends TaskWorkbenchSendMessageInput {
|
||||
waitForCompletion?: boolean;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchCreateSessionCommand {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchCreateSessionAndSendCommand {
|
||||
model?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface TaskWorkbenchSessionCommand {
|
||||
sessionId: string;
|
||||
}
|
||||
|
|
@ -122,15 +122,14 @@ export const task = actor({
|
|||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, input: TaskInput) => ({
|
||||
workspaceId: input.workspaceId,
|
||||
organizationId: input.organizationId,
|
||||
repoId: input.repoId,
|
||||
taskId: input.taskId,
|
||||
repoRemote: input.repoRemote,
|
||||
repoLocalPath: input.repoLocalPath,
|
||||
branchName: input.branchName,
|
||||
title: input.title,
|
||||
task: input.task,
|
||||
providerId: input.providerId,
|
||||
sandboxProviderId: input.sandboxProviderId,
|
||||
agentType: input.agentType,
|
||||
explicitTitle: input.explicitTitle,
|
||||
explicitBranchName: input.explicitBranchName,
|
||||
|
|
@ -143,7 +142,7 @@ export const task = actor({
|
|||
const self = selfTask(c);
|
||||
const result = await self.send(taskWorkflowQueueName("task.command.initialize"), cmd ?? {}, {
|
||||
wait: true,
|
||||
timeout: 5 * 60_000,
|
||||
timeout: 10_000,
|
||||
});
|
||||
return expectQueueResponse<TaskRecord>(result);
|
||||
},
|
||||
|
|
@ -160,7 +159,7 @@ export const task = actor({
|
|||
const self = selfTask(c);
|
||||
const result = await self.send(taskWorkflowQueueName("task.command.attach"), cmd ?? {}, {
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
timeout: 10_000,
|
||||
});
|
||||
return expectQueueResponse<{ target: string; sessionId: string | null }>(result);
|
||||
},
|
||||
|
|
@ -172,7 +171,7 @@ export const task = actor({
|
|||
{},
|
||||
{
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
return expectQueueResponse<{ switchTarget: string }>(result);
|
||||
|
|
@ -236,7 +235,7 @@ export const task = actor({
|
|||
{},
|
||||
{
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -256,27 +255,40 @@ export const task = actor({
|
|||
});
|
||||
},
|
||||
|
||||
async createWorkbenchSession(c, input?: { model?: string }): Promise<{ tabId: string }> {
|
||||
async createWorkbenchSession(c, input?: { model?: string }): Promise<{ sessionId: string }> {
|
||||
const self = selfTask(c);
|
||||
const result = await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.create_session"),
|
||||
{ ...(input?.model ? { model: input.model } : {}) } satisfies TaskWorkbenchCreateSessionCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 5 * 60_000,
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
return expectQueueResponse<{ tabId: string }>(result);
|
||||
return expectQueueResponse<{ sessionId: string }>(result);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fire-and-forget: creates a workbench session and sends the initial message.
|
||||
* Used by createWorkbenchTask so the caller doesn't block on session creation.
|
||||
*/
|
||||
async createWorkbenchSessionAndSend(c, input: { model?: string; text: string }): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.create_session_and_send"),
|
||||
{ model: input.model, text: input.text } satisfies TaskWorkbenchCreateSessionAndSendCommand,
|
||||
{ wait: false },
|
||||
);
|
||||
},
|
||||
|
||||
async renameWorkbenchSession(c, input: TaskWorkbenchRenameSessionInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.rename_session"),
|
||||
{ sessionId: input.tabId, title: input.title } satisfies TaskWorkbenchSessionTitleCommand,
|
||||
{ sessionId: input.sessionId, title: input.title } satisfies TaskWorkbenchSessionTitleCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -285,10 +297,10 @@ export const task = actor({
|
|||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.set_session_unread"),
|
||||
{ sessionId: input.tabId, unread: input.unread } satisfies TaskWorkbenchSessionUnreadCommand,
|
||||
{ sessionId: input.sessionId, unread: input.unread } satisfies TaskWorkbenchSessionUnreadCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -298,13 +310,12 @@ export const task = actor({
|
|||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.update_draft"),
|
||||
{
|
||||
sessionId: input.tabId,
|
||||
sessionId: input.sessionId,
|
||||
text: input.text,
|
||||
attachments: input.attachments,
|
||||
} satisfies TaskWorkbenchUpdateDraftCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -313,36 +324,32 @@ export const task = actor({
|
|||
const self = selfTask(c);
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.change_model"),
|
||||
{ sessionId: input.tabId, model: input.model } satisfies TaskWorkbenchChangeModelCommand,
|
||||
{ sessionId: input.sessionId, model: input.model } satisfies TaskWorkbenchChangeModelCommand,
|
||||
{
|
||||
wait: true,
|
||||
timeout: 20_000,
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
async sendWorkbenchMessage(c, input: TaskWorkbenchSendMessageActionInput): Promise<void> {
|
||||
async sendWorkbenchMessage(c, input: TaskWorkbenchSendMessageInput): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
const result = await self.send(
|
||||
await self.send(
|
||||
taskWorkflowQueueName("task.command.workbench.send_message"),
|
||||
{
|
||||
sessionId: input.tabId,
|
||||
sessionId: input.sessionId,
|
||||
text: input.text,
|
||||
attachments: input.attachments,
|
||||
} satisfies TaskWorkbenchSendMessageCommand,
|
||||
{
|
||||
wait: input.waitForCompletion === true,
|
||||
...(input.waitForCompletion === true ? { timeout: 10 * 60_000 } : {}),
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
if (input.waitForCompletion === true) {
|
||||
expectQueueResponse(result);
|
||||
}
|
||||
},
|
||||
|
||||
async stopWorkbenchSession(c, input: TaskTabCommand): Promise<void> {
|
||||
async stopWorkbenchSession(c, input: TaskSessionCommand): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.stop_session"), { sessionId: input.tabId } satisfies TaskWorkbenchSessionCommand, {
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.stop_session"), { sessionId: input.sessionId } satisfies TaskWorkbenchSessionCommand, {
|
||||
wait: false,
|
||||
});
|
||||
},
|
||||
|
|
@ -355,9 +362,9 @@ export const task = actor({
|
|||
});
|
||||
},
|
||||
|
||||
async closeWorkbenchSession(c, input: TaskTabCommand): Promise<void> {
|
||||
async closeWorkbenchSession(c, input: TaskSessionCommand): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.close_session"), { sessionId: input.tabId } satisfies TaskWorkbenchSessionCommand, {
|
||||
await self.send(taskWorkflowQueueName("task.command.workbench.close_session"), { sessionId: input.sessionId } satisfies TaskWorkbenchSessionCommand, {
|
||||
wait: false,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import { randomUUID } from "node:crypto";
|
|||
import { basename, dirname } from "node:path";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getOrCreateProject, getOrCreateTaskSandbox, getOrCreateWorkspace, getTaskSandbox, selfTask } from "../handles.js";
|
||||
import { getOrCreateRepository, getOrCreateTaskSandbox, getOrCreateOrganization, getTaskSandbox, selfTask } from "../handles.js";
|
||||
import { SANDBOX_REPO_CWD } from "../sandbox/index.js";
|
||||
import { resolveSandboxProviderId } from "../../sandbox-config.js";
|
||||
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
||||
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
|
||||
import { githubRepoFullNameFromRemote } from "../../services/repo.js";
|
||||
import { task as taskTable, taskRuntime, taskSandboxes, taskWorkbenchSessions } from "./db/schema.js";
|
||||
import { getCurrentRecord } from "./workflow/common.js";
|
||||
|
||||
|
|
@ -172,8 +173,7 @@ async function listSessionMetaRows(c: any, options?: { includeClosed?: boolean }
|
|||
const mapped = rows.map((row: any) => ({
|
||||
...row,
|
||||
id: row.sessionId,
|
||||
sessionId: row.sandboxSessionId ?? null,
|
||||
tabId: row.sessionId,
|
||||
sessionId: row.sessionId,
|
||||
sandboxSessionId: row.sandboxSessionId ?? null,
|
||||
status: row.status ?? "ready",
|
||||
errorMessage: row.errorMessage ?? null,
|
||||
|
|
@ -209,8 +209,7 @@ async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
|||
return {
|
||||
...row,
|
||||
id: row.sessionId,
|
||||
sessionId: row.sandboxSessionId ?? null,
|
||||
tabId: row.sessionId,
|
||||
sessionId: row.sessionId,
|
||||
sandboxSessionId: row.sandboxSessionId ?? null,
|
||||
status: row.status ?? "ready",
|
||||
errorMessage: row.errorMessage ?? null,
|
||||
|
|
@ -227,7 +226,7 @@ async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
|||
async function ensureSessionMeta(
|
||||
c: any,
|
||||
params: {
|
||||
tabId: string;
|
||||
sessionId: string;
|
||||
sandboxSessionId?: string | null;
|
||||
model?: string;
|
||||
sessionName?: string;
|
||||
|
|
@ -238,7 +237,7 @@ async function ensureSessionMeta(
|
|||
},
|
||||
): Promise<any> {
|
||||
await ensureWorkbenchSessionTable(c);
|
||||
const existing = await readSessionMeta(c, params.tabId);
|
||||
const existing = await readSessionMeta(c, params.sessionId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
|
@ -251,7 +250,7 @@ async function ensureSessionMeta(
|
|||
await c.db
|
||||
.insert(taskWorkbenchSessions)
|
||||
.values({
|
||||
sessionId: params.tabId,
|
||||
sessionId: params.sessionId,
|
||||
sandboxSessionId: params.sandboxSessionId ?? null,
|
||||
sessionName,
|
||||
model,
|
||||
|
|
@ -271,20 +270,20 @@ async function ensureSessionMeta(
|
|||
})
|
||||
.run();
|
||||
|
||||
return await readSessionMeta(c, params.tabId);
|
||||
return await readSessionMeta(c, params.sessionId);
|
||||
}
|
||||
|
||||
async function updateSessionMeta(c: any, tabId: string, values: Record<string, unknown>): Promise<any> {
|
||||
await ensureSessionMeta(c, { tabId });
|
||||
async function updateSessionMeta(c: any, sessionId: string, values: Record<string, unknown>): Promise<any> {
|
||||
await ensureSessionMeta(c, { sessionId });
|
||||
await c.db
|
||||
.update(taskWorkbenchSessions)
|
||||
.set({
|
||||
...values,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(taskWorkbenchSessions.sessionId, tabId))
|
||||
.where(eq(taskWorkbenchSessions.sessionId, sessionId))
|
||||
.run();
|
||||
return await readSessionMeta(c, tabId);
|
||||
return await readSessionMeta(c, sessionId);
|
||||
}
|
||||
|
||||
async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: string): Promise<any | null> {
|
||||
|
|
@ -296,33 +295,25 @@ async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: strin
|
|||
return await readSessionMeta(c, row.sessionId);
|
||||
}
|
||||
|
||||
async function requireReadySessionMeta(c: any, tabId: string): Promise<any> {
|
||||
const meta = await readSessionMeta(c, tabId);
|
||||
async function requireReadySessionMeta(c: any, sessionId: string): Promise<any> {
|
||||
const meta = await readSessionMeta(c, sessionId);
|
||||
if (!meta) {
|
||||
throw new Error(`Unknown workbench tab: ${tabId}`);
|
||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
||||
}
|
||||
if (meta.status !== "ready" || !meta.sandboxSessionId) {
|
||||
throw new Error(meta.errorMessage ?? "This workbench tab is still preparing");
|
||||
throw new Error(meta.errorMessage ?? "This workbench session is still preparing");
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
async function ensureReadySessionMeta(c: any, tabId: string): Promise<any> {
|
||||
const meta = await readSessionMeta(c, tabId);
|
||||
export function requireSendableSessionMeta(meta: any, sessionId: string): any {
|
||||
if (!meta) {
|
||||
throw new Error(`Unknown workbench tab: ${tabId}`);
|
||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
||||
}
|
||||
|
||||
if (meta.status === "ready" && meta.sandboxSessionId) {
|
||||
return meta;
|
||||
if (meta.status !== "ready" || !meta.sandboxSessionId) {
|
||||
throw new Error(`Session is not ready (status: ${meta.status}). Wait for session provisioning to complete.`);
|
||||
}
|
||||
|
||||
if (meta.status === "error") {
|
||||
throw new Error(meta.errorMessage ?? "This workbench tab failed to prepare");
|
||||
}
|
||||
|
||||
await ensureWorkbenchSession(c, tabId);
|
||||
return await requireReadySessionMeta(c, tabId);
|
||||
return meta;
|
||||
}
|
||||
|
||||
function shellFragment(parts: string[]): string {
|
||||
|
|
@ -339,23 +330,23 @@ async function getTaskSandboxRuntime(
|
|||
): Promise<{
|
||||
sandbox: any;
|
||||
sandboxId: string;
|
||||
providerId: string;
|
||||
sandboxProviderId: string;
|
||||
switchTarget: string;
|
||||
cwd: string;
|
||||
}> {
|
||||
const { config } = getActorRuntimeContext();
|
||||
const sandboxId = stableSandboxId(c);
|
||||
const providerId = resolveSandboxProviderId(config, record.providerId ?? c.state.providerId ?? null);
|
||||
const sandbox = await getOrCreateTaskSandbox(c, c.state.workspaceId, sandboxId, {});
|
||||
const sandboxProviderId = resolveSandboxProviderId(config, record.sandboxProviderId ?? c.state.sandboxProviderId ?? null);
|
||||
const sandbox = await getOrCreateTaskSandbox(c, c.state.organizationId, sandboxId, {});
|
||||
const actorId = typeof sandbox.resolve === "function" ? await sandbox.resolve().catch(() => null) : null;
|
||||
const switchTarget = providerId === "local" ? `sandbox://local/${sandboxId}` : `sandbox://e2b/${sandboxId}`;
|
||||
const switchTarget = sandboxProviderId === "local" ? `sandbox://local/${sandboxId}` : `sandbox://e2b/${sandboxId}`;
|
||||
const now = Date.now();
|
||||
|
||||
await c.db
|
||||
.insert(taskSandboxes)
|
||||
.values({
|
||||
sandboxId,
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
sandboxActorId: typeof actorId === "string" ? actorId : null,
|
||||
switchTarget,
|
||||
cwd: SANDBOX_REPO_CWD,
|
||||
|
|
@ -366,7 +357,7 @@ async function getTaskSandboxRuntime(
|
|||
.onConflictDoUpdate({
|
||||
target: taskSandboxes.sandboxId,
|
||||
set: {
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
sandboxActorId: typeof actorId === "string" ? actorId : null,
|
||||
switchTarget,
|
||||
cwd: SANDBOX_REPO_CWD,
|
||||
|
|
@ -389,7 +380,7 @@ async function getTaskSandboxRuntime(
|
|||
return {
|
||||
sandbox,
|
||||
sandboxId,
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
switchTarget,
|
||||
cwd: SANDBOX_REPO_CWD,
|
||||
};
|
||||
|
|
@ -400,17 +391,10 @@ async function ensureSandboxRepo(c: any, sandbox: any, record: any): Promise<voi
|
|||
throw new Error("cannot prepare a sandbox repo before the task branch exists");
|
||||
}
|
||||
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const auth = await resolveWorkspaceGithubAuth(c, c.state.workspaceId);
|
||||
let repoLocalPath = c.state.repoLocalPath;
|
||||
if (!repoLocalPath) {
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, c.state.repoId, c.state.repoRemote);
|
||||
const ensured = await project.ensure({ remoteUrl: c.state.repoRemote });
|
||||
repoLocalPath = ensured.localPath;
|
||||
c.state.repoLocalPath = repoLocalPath;
|
||||
}
|
||||
|
||||
const baseRef = await driver.git.remoteDefaultBaseRef(repoLocalPath);
|
||||
const auth = await resolveOrganizationGithubAuth(c, c.state.organizationId);
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, c.state.repoId, c.state.repoRemote);
|
||||
const metadata = await repository.getRepositoryMetadata({});
|
||||
const baseRef = metadata.defaultBranch ?? "main";
|
||||
const sandboxRepoRoot = dirname(SANDBOX_REPO_CWD);
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
|
|
@ -665,7 +649,7 @@ async function readSessionTranscript(c: any, record: any, sessionId: string) {
|
|||
return [];
|
||||
}
|
||||
|
||||
const sandbox = getTaskSandbox(c, c.state.workspaceId, sandboxId);
|
||||
const sandbox = getTaskSandbox(c, c.state.organizationId, sandboxId);
|
||||
const page = await sandbox.getEvents({
|
||||
sessionId,
|
||||
limit: 100,
|
||||
|
|
@ -681,8 +665,8 @@ async function readSessionTranscript(c: any, record: any, sessionId: string) {
|
|||
}));
|
||||
}
|
||||
|
||||
async function writeSessionTranscript(c: any, tabId: string, transcript: Array<any>): Promise<void> {
|
||||
await updateSessionMeta(c, tabId, {
|
||||
async function writeSessionTranscript(c: any, sessionId: string, transcript: Array<any>): Promise<void> {
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
transcriptJson: JSON.stringify(transcript),
|
||||
transcriptUpdatedAt: Date.now(),
|
||||
});
|
||||
|
|
@ -697,12 +681,12 @@ async function enqueueWorkbenchRefresh(
|
|||
await self.send(command, body, { wait: false });
|
||||
}
|
||||
|
||||
async function enqueueWorkbenchEnsureSession(c: any, tabId: string): Promise<void> {
|
||||
async function enqueueWorkbenchEnsureSession(c: any, sessionId: string): Promise<void> {
|
||||
const self = selfTask(c);
|
||||
await self.send(
|
||||
"task.command.workbench.ensure_session",
|
||||
{
|
||||
tabId,
|
||||
sessionId,
|
||||
},
|
||||
{
|
||||
wait: false,
|
||||
|
|
@ -750,8 +734,8 @@ async function readPullRequestSummary(c: any, branchName: string | null) {
|
|||
}
|
||||
|
||||
try {
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, c.state.repoId, c.state.repoRemote);
|
||||
return await project.getPullRequestForBranch({ branchName });
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, c.state.repoId, c.state.repoRemote);
|
||||
return await repository.getPullRequestForBranch({ branchName });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -762,7 +746,7 @@ export async function ensureWorkbenchSeeded(c: any): Promise<any> {
|
|||
const record = await getCurrentRecord({ db: c.db, state: c.state });
|
||||
if (record.activeSessionId) {
|
||||
await ensureSessionMeta(c, {
|
||||
tabId: record.activeSessionId,
|
||||
sessionId: record.activeSessionId,
|
||||
sandboxSessionId: record.activeSessionId,
|
||||
model: defaultModelForAgent(record.agentType),
|
||||
sessionName: "Session 1",
|
||||
|
|
@ -791,7 +775,8 @@ function buildSessionSummary(record: any, meta: any): any {
|
|||
|
||||
return {
|
||||
id: meta.id,
|
||||
sessionId: derivedSandboxSessionId,
|
||||
sessionId: meta.sessionId,
|
||||
sandboxSessionId: derivedSandboxSessionId,
|
||||
sessionName: meta.sessionName,
|
||||
agent: agentKindForModel(meta.model),
|
||||
model: meta.model,
|
||||
|
|
@ -806,9 +791,8 @@ function buildSessionSummary(record: any, meta: any): any {
|
|||
function buildSessionDetailFromMeta(record: any, meta: any): any {
|
||||
const summary = buildSessionSummary(record, meta);
|
||||
return {
|
||||
sessionId: meta.tabId,
|
||||
tabId: meta.tabId,
|
||||
sandboxSessionId: summary.sessionId,
|
||||
sessionId: meta.sessionId,
|
||||
sandboxSessionId: summary.sandboxSessionId ?? null,
|
||||
sessionName: summary.sessionName,
|
||||
agent: summary.agent,
|
||||
model: summary.model,
|
||||
|
|
@ -828,7 +812,7 @@ function buildSessionDetailFromMeta(record: any, meta: any): any {
|
|||
|
||||
/**
|
||||
* Builds a WorkbenchTaskSummary from local task actor state. Task actors push
|
||||
* this to the parent workspace actor so workspace sidebar reads stay local.
|
||||
* this to the parent organization actor so organization sidebar reads stay local.
|
||||
*/
|
||||
export async function buildTaskSummary(c: any): Promise<any> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
|
|
@ -874,7 +858,7 @@ export async function buildTaskDetail(c: any): Promise<any> {
|
|||
fileTree: gitState.fileTree,
|
||||
minutesUsed: 0,
|
||||
sandboxes: (record.sandboxes ?? []).map((sandbox: any) => ({
|
||||
providerId: sandbox.providerId,
|
||||
sandboxProviderId: sandbox.sandboxProviderId,
|
||||
sandboxId: sandbox.sandboxId,
|
||||
cwd: sandbox.cwd ?? null,
|
||||
})),
|
||||
|
|
@ -883,13 +867,13 @@ export async function buildTaskDetail(c: any): Promise<any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Builds a WorkbenchSessionDetail for a specific session tab.
|
||||
* Builds a WorkbenchSessionDetail for a specific session.
|
||||
*/
|
||||
export async function buildSessionDetail(c: any, tabId: string): Promise<any> {
|
||||
export async function buildSessionDetail(c: any, sessionId: string): Promise<any> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const meta = await readSessionMeta(c, tabId);
|
||||
const meta = await readSessionMeta(c, sessionId);
|
||||
if (!meta || meta.closed) {
|
||||
throw new Error(`Unknown workbench session tab: ${tabId}`);
|
||||
throw new Error(`Unknown workbench session: ${sessionId}`);
|
||||
}
|
||||
|
||||
if (!meta.sandboxSessionId) {
|
||||
|
|
@ -899,7 +883,7 @@ export async function buildSessionDetail(c: any, tabId: string): Promise<any> {
|
|||
try {
|
||||
const transcript = await readSessionTranscript(c, record, meta.sandboxSessionId);
|
||||
if (JSON.stringify(meta.transcript ?? []) !== JSON.stringify(transcript)) {
|
||||
await writeSessionTranscript(c, meta.tabId, transcript);
|
||||
await writeSessionTranscript(c, meta.sessionId, transcript);
|
||||
return buildSessionDetailFromMeta(record, {
|
||||
...meta,
|
||||
transcript,
|
||||
|
|
@ -921,21 +905,21 @@ export async function getTaskDetail(c: any): Promise<any> {
|
|||
return await buildTaskDetail(c);
|
||||
}
|
||||
|
||||
export async function getSessionDetail(c: any, tabId: string): Promise<any> {
|
||||
return await buildSessionDetail(c, tabId);
|
||||
export async function getSessionDetail(c: any, sessionId: string): Promise<any> {
|
||||
return await buildSessionDetail(c, sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the old notifyWorkbenchUpdated pattern.
|
||||
*
|
||||
* The task actor emits two kinds of updates:
|
||||
* - Push summary state up to the parent workspace actor so the sidebar
|
||||
* - Push summary state up to the parent organization actor so the sidebar
|
||||
* materialized projection stays current.
|
||||
* - Broadcast full detail/session payloads down to direct task subscribers.
|
||||
*/
|
||||
export async function broadcastTaskUpdate(c: any, options?: { sessionId?: string }): Promise<void> {
|
||||
const workspace = await getOrCreateWorkspace(c, c.state.workspaceId);
|
||||
await workspace.applyTaskSummaryUpdate({ taskSummary: await buildTaskSummary(c) });
|
||||
const organization = await getOrCreateOrganization(c, c.state.organizationId);
|
||||
await organization.applyTaskSummaryUpdate({ taskSummary: await buildTaskSummary(c) });
|
||||
c.broadcast("taskUpdated", {
|
||||
type: "taskDetailUpdated",
|
||||
detail: await buildTaskDetail(c),
|
||||
|
|
@ -964,8 +948,8 @@ export async function refreshWorkbenchSessionTranscript(c: any, sessionId: strin
|
|||
}
|
||||
|
||||
const transcript = await readSessionTranscript(c, record, meta.sandboxSessionId);
|
||||
await writeSessionTranscript(c, meta.tabId, transcript);
|
||||
await broadcastTaskUpdate(c, { sessionId: meta.tabId });
|
||||
await writeSessionTranscript(c, meta.sessionId, transcript);
|
||||
await broadcastTaskUpdate(c, { sessionId: meta.sessionId });
|
||||
}
|
||||
|
||||
export async function renameWorkbenchTask(c: any, value: string): Promise<void> {
|
||||
|
|
@ -1029,31 +1013,31 @@ export async function renameWorkbenchBranch(c: any, value: string): Promise<void
|
|||
.run();
|
||||
c.state.branchName = nextBranch;
|
||||
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, c.state.repoId, c.state.repoRemote);
|
||||
await project.registerTaskBranch({
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, c.state.repoId, c.state.repoRemote);
|
||||
await repository.registerTaskBranch({
|
||||
taskId: c.state.taskId,
|
||||
branchName: nextBranch,
|
||||
});
|
||||
await broadcastTaskUpdate(c);
|
||||
}
|
||||
|
||||
export async function createWorkbenchSession(c: any, model?: string): Promise<{ tabId: string }> {
|
||||
const tabId = `tab-${randomUUID()}`;
|
||||
export async function createWorkbenchSession(c: any, model?: string): Promise<{ sessionId: string }> {
|
||||
const sessionId = `session-${randomUUID()}`;
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
await ensureSessionMeta(c, {
|
||||
tabId,
|
||||
sessionId,
|
||||
model: model ?? defaultModelForAgent(record.agentType),
|
||||
sandboxSessionId: null,
|
||||
status: pendingWorkbenchSessionStatus(record),
|
||||
created: false,
|
||||
});
|
||||
await broadcastTaskUpdate(c, { sessionId: tabId });
|
||||
await enqueueWorkbenchEnsureSession(c, tabId);
|
||||
return { tabId };
|
||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||
await enqueueWorkbenchEnsureSession(c, sessionId);
|
||||
return { sessionId };
|
||||
}
|
||||
|
||||
export async function ensureWorkbenchSession(c: any, tabId: string, model?: string): Promise<void> {
|
||||
const meta = await readSessionMeta(c, tabId);
|
||||
export async function ensureWorkbenchSession(c: any, sessionId: string, model?: string): Promise<void> {
|
||||
const meta = await readSessionMeta(c, sessionId);
|
||||
if (!meta || meta.closed) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1063,12 +1047,12 @@ export async function ensureWorkbenchSession(c: any, tabId: string, model?: stri
|
|||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
sessionId: meta.sandboxSessionId,
|
||||
});
|
||||
await broadcastTaskUpdate(c, { sessionId: tabId });
|
||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||
return;
|
||||
}
|
||||
|
||||
await updateSessionMeta(c, tabId, {
|
||||
sandboxSessionId: meta.sandboxSessionId ?? tabId,
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
sandboxSessionId: meta.sandboxSessionId ?? sessionId,
|
||||
status: "pending_session_create",
|
||||
errorMessage: null,
|
||||
});
|
||||
|
|
@ -1077,7 +1061,7 @@ export async function ensureWorkbenchSession(c: any, tabId: string, model?: stri
|
|||
const runtime = await getTaskSandboxRuntime(c, record);
|
||||
await ensureSandboxRepo(c, runtime.sandbox, record);
|
||||
await runtime.sandbox.createSession({
|
||||
id: meta.sandboxSessionId ?? tabId,
|
||||
id: meta.sandboxSessionId ?? sessionId,
|
||||
agent: agentTypeForModel(model ?? meta.model ?? defaultModelForAgent(record.agentType)),
|
||||
model: model ?? meta.model ?? defaultModelForAgent(record.agentType),
|
||||
sessionInit: {
|
||||
|
|
@ -1085,22 +1069,22 @@ export async function ensureWorkbenchSession(c: any, tabId: string, model?: stri
|
|||
},
|
||||
});
|
||||
|
||||
await updateSessionMeta(c, tabId, {
|
||||
sandboxSessionId: meta.sandboxSessionId ?? tabId,
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
sandboxSessionId: meta.sandboxSessionId ?? sessionId,
|
||||
status: "ready",
|
||||
errorMessage: null,
|
||||
});
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
sessionId: meta.sandboxSessionId ?? tabId,
|
||||
sessionId: meta.sandboxSessionId ?? sessionId,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateSessionMeta(c, tabId, {
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
status: "error",
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
await broadcastTaskUpdate(c, { sessionId: tabId });
|
||||
await broadcastTaskUpdate(c, { sessionId: sessionId });
|
||||
}
|
||||
|
||||
export async function enqueuePendingWorkbenchSessions(c: any): Promise<void> {
|
||||
|
|
@ -1113,7 +1097,7 @@ export async function enqueuePendingWorkbenchSessions(c: any): Promise<void> {
|
|||
await self.send(
|
||||
"task.command.workbench.ensure_session",
|
||||
{
|
||||
tabId: row.tabId,
|
||||
sessionId: row.sessionId,
|
||||
model: row.model,
|
||||
},
|
||||
{
|
||||
|
|
@ -1167,7 +1151,7 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
|||
let shouldEnsure = nextMeta.status === "pending_provision" || nextMeta.status === "pending_session_create" || nextMeta.status === "error";
|
||||
|
||||
if (shouldRecreateSessionForModelChange(nextMeta)) {
|
||||
const sandbox = getTaskSandbox(c, c.state.workspaceId, stableSandboxId(c));
|
||||
const sandbox = getTaskSandbox(c, c.state.organizationId, stableSandboxId(c));
|
||||
await sandbox.destroySession(nextMeta.sandboxSessionId);
|
||||
nextMeta = await updateSessionMeta(c, sessionId, {
|
||||
sandboxSessionId: null,
|
||||
|
|
@ -1179,7 +1163,7 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
|||
});
|
||||
shouldEnsure = true;
|
||||
} else if (nextMeta.status === "ready" && nextMeta.sandboxSessionId) {
|
||||
const sandbox = getTaskSandbox(c, c.state.workspaceId, stableSandboxId(c));
|
||||
const sandbox = getTaskSandbox(c, c.state.organizationId, stableSandboxId(c));
|
||||
if (typeof sandbox.rawSendSessionMethod === "function") {
|
||||
try {
|
||||
await sandbox.rawSendSessionMethod(nextMeta.sandboxSessionId, "session/set_config_option", {
|
||||
|
|
@ -1204,7 +1188,7 @@ export async function changeWorkbenchModel(c: any, sessionId: string, model: str
|
|||
}
|
||||
|
||||
export async function sendWorkbenchMessage(c: any, sessionId: string, text: string, attachments: Array<any>): Promise<void> {
|
||||
const meta = await ensureReadySessionMeta(c, sessionId);
|
||||
const meta = requireSendableSessionMeta(await readSessionMeta(c, sessionId), sessionId);
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const runtime = await getTaskSandboxRuntime(c, record);
|
||||
await ensureSandboxRepo(c, runtime.sandbox, record);
|
||||
|
|
@ -1253,7 +1237,7 @@ export async function sendWorkbenchMessage(c: any, sessionId: string, text: stri
|
|||
|
||||
export async function stopWorkbenchSession(c: any, sessionId: string): Promise<void> {
|
||||
const meta = await requireReadySessionMeta(c, sessionId);
|
||||
const sandbox = getTaskSandbox(c, c.state.workspaceId, stableSandboxId(c));
|
||||
const sandbox = getTaskSandbox(c, c.state.organizationId, stableSandboxId(c));
|
||||
await sandbox.destroySession(meta.sandboxSessionId);
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
thinkingSinceMs: null,
|
||||
|
|
@ -1263,7 +1247,7 @@ export async function stopWorkbenchSession(c: any, sessionId: string): Promise<v
|
|||
|
||||
export async function syncWorkbenchSessionStatus(c: any, sessionId: string, status: "running" | "idle" | "error", at: number): Promise<void> {
|
||||
const record = await ensureWorkbenchSeeded(c);
|
||||
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await ensureSessionMeta(c, { tabId: sessionId, sandboxSessionId: sessionId }));
|
||||
const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await ensureSessionMeta(c, { sessionId: sessionId, sandboxSessionId: sessionId }));
|
||||
let changed = false;
|
||||
|
||||
if (record.activeSessionId === sessionId || record.activeSessionId === meta.sandboxSessionId) {
|
||||
|
|
@ -1317,13 +1301,13 @@ export async function syncWorkbenchSessionStatus(c: any, sessionId: string, stat
|
|||
}
|
||||
|
||||
if (changed) {
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
sessionId,
|
||||
});
|
||||
if (status !== "running") {
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", {
|
||||
sessionId,
|
||||
});
|
||||
await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_derived", {});
|
||||
}
|
||||
await broadcastTaskUpdate(c, { sessionId: meta.tabId });
|
||||
await broadcastTaskUpdate(c, { sessionId: meta.sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1339,7 +1323,7 @@ export async function closeWorkbenchSession(c: any, sessionId: string): Promise<
|
|||
return;
|
||||
}
|
||||
if (meta.sandboxSessionId) {
|
||||
const sandbox = getTaskSandbox(c, c.state.workspaceId, stableSandboxId(c));
|
||||
const sandbox = getTaskSandbox(c, c.state.organizationId, stableSandboxId(c));
|
||||
await sandbox.destroySession(meta.sandboxSessionId);
|
||||
}
|
||||
await updateSessionMeta(c, sessionId, {
|
||||
|
|
@ -1365,10 +1349,10 @@ export async function markWorkbenchUnread(c: any): Promise<void> {
|
|||
if (!latest) {
|
||||
return;
|
||||
}
|
||||
await updateSessionMeta(c, latest.tabId, {
|
||||
await updateSessionMeta(c, latest.sessionId, {
|
||||
unread: 1,
|
||||
});
|
||||
await broadcastTaskUpdate(c, { sessionId: latest.tabId });
|
||||
await broadcastTaskUpdate(c, { sessionId: latest.sessionId });
|
||||
}
|
||||
|
||||
export async function publishWorkbenchPr(c: any): Promise<void> {
|
||||
|
|
@ -1376,17 +1360,17 @@ export async function publishWorkbenchPr(c: any): Promise<void> {
|
|||
if (!record.branchName) {
|
||||
throw new Error("cannot publish PR without a branch");
|
||||
}
|
||||
let repoLocalPath = c.state.repoLocalPath;
|
||||
if (!repoLocalPath) {
|
||||
const project = await getOrCreateProject(c, c.state.workspaceId, c.state.repoId, c.state.repoRemote);
|
||||
const result = await project.ensure({ remoteUrl: c.state.repoRemote });
|
||||
repoLocalPath = result.localPath;
|
||||
c.state.repoLocalPath = repoLocalPath;
|
||||
const repository = await getOrCreateRepository(c, c.state.organizationId, c.state.repoId, c.state.repoRemote);
|
||||
const metadata = await repository.getRepositoryMetadata({});
|
||||
const repoFullName = metadata.fullName ?? githubRepoFullNameFromRemote(c.state.repoRemote);
|
||||
if (!repoFullName) {
|
||||
throw new Error(`Unable to resolve GitHub repository for ${c.state.repoRemote}`);
|
||||
}
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const auth = await resolveWorkspaceGithubAuth(c, c.state.workspaceId);
|
||||
const created = await driver.github.createPr(repoLocalPath, record.branchName, record.title ?? c.state.task, undefined, {
|
||||
const auth = await resolveOrganizationGithubAuth(c, c.state.organizationId);
|
||||
await driver.github.createPr(repoFullName, record.branchName, record.title ?? c.state.task, undefined, {
|
||||
githubToken: auth?.githubToken ?? null,
|
||||
baseBranch: metadata.defaultBranch ?? undefined,
|
||||
});
|
||||
await c.db
|
||||
.update(taskTable)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export async function handleAttachActivity(loopCtx: any, msg: any): Promise<void
|
|||
|
||||
if (record.activeSandboxId) {
|
||||
try {
|
||||
const sandbox = getTaskSandbox(loopCtx, loopCtx.state.workspaceId, record.activeSandboxId);
|
||||
const sandbox = getTaskSandbox(loopCtx, loopCtx.state.organizationId, record.activeSandboxId);
|
||||
const connection = await sandbox.sandboxAgentConnection();
|
||||
if (typeof connection?.endpoint === "string" && connection.endpoint.length > 0) {
|
||||
target = connection.endpoint;
|
||||
|
|
@ -78,9 +78,9 @@ export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<voi
|
|||
|
||||
if (record.activeSandboxId) {
|
||||
await setTaskState(loopCtx, "archive_release_sandbox", "releasing sandbox");
|
||||
void withTimeout(getTaskSandbox(loopCtx, loopCtx.state.workspaceId, record.activeSandboxId).destroy(), 45_000, "sandbox destroy").catch((error) => {
|
||||
void withTimeout(getTaskSandbox(loopCtx, loopCtx.state.organizationId, record.activeSandboxId).destroy(), 45_000, "sandbox destroy").catch((error) => {
|
||||
logActorWarning("task.commands", "failed to release sandbox during archive", {
|
||||
workspaceId: loopCtx.state.workspaceId,
|
||||
organizationId: loopCtx.state.organizationId,
|
||||
repoId: loopCtx.state.repoId,
|
||||
taskId: loopCtx.state.taskId,
|
||||
sandboxId: record.activeSandboxId,
|
||||
|
|
@ -106,7 +106,7 @@ export async function killDestroySandboxActivity(loopCtx: any): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
await getTaskSandbox(loopCtx, loopCtx.state.workspaceId, record.activeSandboxId).destroy();
|
||||
await getTaskSandbox(loopCtx, loopCtx.state.organizationId, record.activeSandboxId).destroy();
|
||||
}
|
||||
|
||||
export async function killWriteDbActivity(loopCtx: any, msg: any): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
branchName: taskTable.branchName,
|
||||
title: taskTable.title,
|
||||
task: taskTable.task,
|
||||
providerId: taskTable.providerId,
|
||||
sandboxProviderId: taskTable.sandboxProviderId,
|
||||
status: taskTable.status,
|
||||
statusMessage: taskRuntime.statusMessage,
|
||||
activeSandboxId: taskRuntime.activeSandboxId,
|
||||
|
|
@ -115,7 +115,7 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
const sandboxes = await db
|
||||
.select({
|
||||
sandboxId: taskSandboxes.sandboxId,
|
||||
providerId: taskSandboxes.providerId,
|
||||
sandboxProviderId: taskSandboxes.sandboxProviderId,
|
||||
sandboxActorId: taskSandboxes.sandboxActorId,
|
||||
switchTarget: taskSandboxes.switchTarget,
|
||||
cwd: taskSandboxes.cwd,
|
||||
|
|
@ -126,21 +126,21 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
.all();
|
||||
|
||||
return {
|
||||
workspaceId: ctx.state.workspaceId,
|
||||
organizationId: ctx.state.organizationId,
|
||||
repoId: ctx.state.repoId,
|
||||
repoRemote: ctx.state.repoRemote,
|
||||
taskId: ctx.state.taskId,
|
||||
branchName: row.branchName,
|
||||
title: row.title,
|
||||
task: row.task,
|
||||
providerId: row.providerId,
|
||||
sandboxProviderId: row.sandboxProviderId,
|
||||
status: row.status,
|
||||
statusMessage: row.statusMessage ?? null,
|
||||
activeSandboxId: row.activeSandboxId ?? null,
|
||||
activeSessionId: row.activeSessionId ?? null,
|
||||
sandboxes: sandboxes.map((sb) => ({
|
||||
sandboxId: sb.sandboxId,
|
||||
providerId: sb.providerId,
|
||||
sandboxProviderId: sb.sandboxProviderId,
|
||||
sandboxActorId: sb.sandboxActorId ?? null,
|
||||
switchTarget: sb.switchTarget,
|
||||
cwd: sb.cwd ?? null,
|
||||
|
|
@ -165,8 +165,8 @@ export async function getCurrentRecord(ctx: any): Promise<TaskRecord> {
|
|||
|
||||
export async function appendHistory(ctx: any, kind: string, payload: Record<string, unknown>): Promise<void> {
|
||||
const client = ctx.client();
|
||||
const history = await client.history.getOrCreate(historyKey(ctx.state.workspaceId, ctx.state.repoId), {
|
||||
createWithInput: { workspaceId: ctx.state.workspaceId, repoId: ctx.state.repoId },
|
||||
const history = await client.history.getOrCreate(historyKey(ctx.state.organizationId, ctx.state.repoId), {
|
||||
createWithInput: { organizationId: ctx.state.organizationId, repoId: ctx.state.repoId },
|
||||
});
|
||||
await history.append({
|
||||
kind,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
import { Loop } from "rivetkit/workflow";
|
||||
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
||||
import { getCurrentRecord } from "./common.js";
|
||||
import {
|
||||
initAssertNameActivity,
|
||||
initBootstrapDbActivity,
|
||||
initCompleteActivity,
|
||||
initEnqueueProvisionActivity,
|
||||
initEnsureNameActivity,
|
||||
initFailedActivity,
|
||||
} from "./init.js";
|
||||
import { initBootstrapDbActivity, initCompleteActivity, initEnqueueProvisionActivity, initFailedActivity } from "./init.js";
|
||||
import {
|
||||
handleArchiveActivity,
|
||||
handleAttachActivity,
|
||||
|
|
@ -67,12 +60,8 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
|
|||
await loopCtx.removed("init-failed", "step");
|
||||
await loopCtx.removed("init-failed-v2", "step");
|
||||
try {
|
||||
await loopCtx.step({
|
||||
name: "init-ensure-name",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => initEnsureNameActivity(loopCtx),
|
||||
});
|
||||
await loopCtx.step("init-assert-name", async () => initAssertNameActivity(loopCtx));
|
||||
await loopCtx.removed("init-ensure-name", "step");
|
||||
await loopCtx.removed("init-assert-name", "step");
|
||||
await loopCtx.removed("init-create-sandbox", "step");
|
||||
await loopCtx.removed("init-ensure-agent", "step");
|
||||
await loopCtx.removed("init-start-sandbox-instance", "step");
|
||||
|
|
@ -156,11 +145,31 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
|
|||
}
|
||||
},
|
||||
|
||||
"task.command.workbench.create_session_and_send": async (loopCtx, msg) => {
|
||||
try {
|
||||
const created = await loopCtx.step({
|
||||
name: "workbench-create-session-for-send",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => createWorkbenchSession(loopCtx, msg.body?.model),
|
||||
});
|
||||
await loopCtx.step({
|
||||
name: "workbench-send-initial-message",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => sendWorkbenchMessage(loopCtx, created.sessionId, msg.body.text, []),
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("task.workflow", "create_session_and_send failed", {
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
||||
"task.command.workbench.ensure_session": async (loopCtx, msg) => {
|
||||
await loopCtx.step({
|
||||
name: "workbench-ensure-session",
|
||||
timeout: 5 * 60_000,
|
||||
run: async () => ensureWorkbenchSession(loopCtx, msg.body.tabId, msg.body?.model),
|
||||
run: async () => ensureWorkbenchSession(loopCtx, msg.body.sessionId, msg.body?.model),
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
},
|
||||
|
|
@ -269,7 +278,16 @@ export async function runTaskWorkflow(ctx: any): Promise<void> {
|
|||
}
|
||||
const handler = commandHandlers[msg.name as TaskQueueName];
|
||||
if (handler) {
|
||||
await handler(loopCtx, msg);
|
||||
try {
|
||||
await handler(loopCtx, msg);
|
||||
} catch (error) {
|
||||
const message = resolveErrorMessage(error);
|
||||
logActorWarning("task.workflow", "task workflow command failed", {
|
||||
queueName: msg.name,
|
||||
error: message,
|
||||
});
|
||||
await msg.complete({ error: message }).catch(() => {});
|
||||
}
|
||||
}
|
||||
return Loop.continue(undefined);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
// @ts-nocheck
|
||||
import { eq } from "drizzle-orm";
|
||||
import { resolveCreateFlowDecision } from "../../../services/create-flow.js";
|
||||
import { resolveWorkspaceGithubAuth } from "../../../services/github-auth.js";
|
||||
import { getActorRuntimeContext } from "../../context.js";
|
||||
import { getOrCreateHistory, getOrCreateProject, selfTask } from "../../handles.js";
|
||||
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
||||
import { getOrCreateHistory, selfTask } from "../../handles.js";
|
||||
import { resolveErrorMessage } from "../../logging.js";
|
||||
import { defaultSandboxProviderId } from "../../../sandbox-config.js";
|
||||
import { task as taskTable, taskRuntime } from "../db/schema.js";
|
||||
import { TASK_ROW_ID, appendHistory, collectErrorMessages, resolveErrorDetail, setTaskState } from "./common.js";
|
||||
|
|
@ -19,9 +17,8 @@ async function ensureTaskRuntimeCacheColumns(db: any): Promise<void> {
|
|||
|
||||
export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<void> {
|
||||
const { config } = getActorRuntimeContext();
|
||||
const providerId = body?.providerId ?? loopCtx.state.providerId ?? defaultSandboxProviderId(config);
|
||||
const sandboxProviderId = body?.sandboxProviderId ?? loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
const now = Date.now();
|
||||
const initialStatusMessage = loopCtx.state.branchName && loopCtx.state.title ? "provisioning" : "naming";
|
||||
|
||||
await ensureTaskRuntimeCacheColumns(loopCtx.db);
|
||||
|
||||
|
|
@ -32,7 +29,7 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
branchName: loopCtx.state.branchName,
|
||||
title: loopCtx.state.title,
|
||||
task: loopCtx.state.task,
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
status: "init_bootstrap_db",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
createdAt: now,
|
||||
|
|
@ -44,7 +41,7 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
branchName: loopCtx.state.branchName,
|
||||
title: loopCtx.state.title,
|
||||
task: loopCtx.state.task,
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
status: "init_bootstrap_db",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
updatedAt: now,
|
||||
|
|
@ -60,7 +57,7 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
activeSessionId: null,
|
||||
activeSwitchTarget: null,
|
||||
activeCwd: null,
|
||||
statusMessage: initialStatusMessage,
|
||||
statusMessage: "provisioning",
|
||||
gitStateJson: null,
|
||||
gitStateUpdatedAt: null,
|
||||
provisionStage: "queued",
|
||||
|
|
@ -74,7 +71,7 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
|||
activeSessionId: null,
|
||||
activeSwitchTarget: null,
|
||||
activeCwd: null,
|
||||
statusMessage: initialStatusMessage,
|
||||
statusMessage: "provisioning",
|
||||
provisionStage: "queued",
|
||||
provisionStageUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
|
|
@ -102,7 +99,7 @@ export async function initEnqueueProvisionActivity(loopCtx: any, body: any): Pro
|
|||
});
|
||||
} catch (error) {
|
||||
logActorWarning("task.init", "background provision command failed", {
|
||||
workspaceId: loopCtx.state.workspaceId,
|
||||
organizationId: loopCtx.state.organizationId,
|
||||
repoId: loopCtx.state.repoId,
|
||||
taskId: loopCtx.state.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
|
|
@ -111,106 +108,10 @@ export async function initEnqueueProvisionActivity(loopCtx: any, body: any): Pro
|
|||
}
|
||||
}
|
||||
|
||||
export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
|
||||
await setTaskState(loopCtx, "init_ensure_name", "determining title and branch");
|
||||
const existing = await loopCtx.db
|
||||
.select({
|
||||
branchName: taskTable.branchName,
|
||||
title: taskTable.title,
|
||||
})
|
||||
.from(taskTable)
|
||||
.where(eq(taskTable.id, TASK_ROW_ID))
|
||||
.get();
|
||||
|
||||
if (existing?.branchName && existing?.title) {
|
||||
loopCtx.state.branchName = existing.branchName;
|
||||
loopCtx.state.title = existing.title;
|
||||
return;
|
||||
}
|
||||
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const auth = await resolveWorkspaceGithubAuth(loopCtx, loopCtx.state.workspaceId);
|
||||
let repoLocalPath = loopCtx.state.repoLocalPath;
|
||||
if (!repoLocalPath) {
|
||||
const project = await getOrCreateProject(loopCtx, loopCtx.state.workspaceId, loopCtx.state.repoId, loopCtx.state.repoRemote);
|
||||
const result = await project.ensure({ remoteUrl: loopCtx.state.repoRemote });
|
||||
repoLocalPath = result.localPath;
|
||||
loopCtx.state.repoLocalPath = repoLocalPath;
|
||||
}
|
||||
|
||||
try {
|
||||
await driver.git.fetch(repoLocalPath, { githubToken: auth?.githubToken ?? null });
|
||||
} catch (error) {
|
||||
logActorWarning("task.init", "fetch before naming failed", {
|
||||
workspaceId: loopCtx.state.workspaceId,
|
||||
repoId: loopCtx.state.repoId,
|
||||
taskId: loopCtx.state.taskId,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
|
||||
const remoteBranches = (await driver.git.listRemoteBranches(repoLocalPath, { githubToken: auth?.githubToken ?? null })).map(
|
||||
(branch: any) => branch.branchName,
|
||||
);
|
||||
const project = await getOrCreateProject(loopCtx, loopCtx.state.workspaceId, loopCtx.state.repoId, loopCtx.state.repoRemote);
|
||||
const reservedBranches = await project.listReservedBranches({});
|
||||
const resolved = resolveCreateFlowDecision({
|
||||
task: loopCtx.state.task,
|
||||
explicitTitle: loopCtx.state.explicitTitle ?? undefined,
|
||||
explicitBranchName: loopCtx.state.explicitBranchName ?? undefined,
|
||||
localBranches: remoteBranches,
|
||||
taskBranches: reservedBranches,
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
await loopCtx.db
|
||||
.update(taskTable)
|
||||
.set({
|
||||
branchName: resolved.branchName,
|
||||
title: resolved.title,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(taskTable.id, TASK_ROW_ID))
|
||||
.run();
|
||||
|
||||
loopCtx.state.branchName = resolved.branchName;
|
||||
loopCtx.state.title = resolved.title;
|
||||
loopCtx.state.explicitTitle = null;
|
||||
loopCtx.state.explicitBranchName = null;
|
||||
|
||||
await loopCtx.db
|
||||
.update(taskRuntime)
|
||||
.set({
|
||||
statusMessage: "provisioning",
|
||||
provisionStage: "repo_prepared",
|
||||
provisionStageUpdatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
||||
.run();
|
||||
|
||||
await project.registerTaskBranch({
|
||||
taskId: loopCtx.state.taskId,
|
||||
branchName: resolved.branchName,
|
||||
});
|
||||
|
||||
await appendHistory(loopCtx, "task.named", {
|
||||
title: resolved.title,
|
||||
branchName: resolved.branchName,
|
||||
});
|
||||
}
|
||||
|
||||
export async function initAssertNameActivity(loopCtx: any): Promise<void> {
|
||||
await setTaskState(loopCtx, "init_assert_name", "validating naming");
|
||||
if (!loopCtx.state.branchName) {
|
||||
throw new Error("task branchName is not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
export async function initCompleteActivity(loopCtx: any, body: any): Promise<void> {
|
||||
const now = Date.now();
|
||||
const { config } = getActorRuntimeContext();
|
||||
const providerId = body?.providerId ?? loopCtx.state.providerId ?? defaultSandboxProviderId(config);
|
||||
const sandboxProviderId = body?.sandboxProviderId ?? loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
|
||||
await setTaskState(loopCtx, "init_complete", "task initialized");
|
||||
await loopCtx.db
|
||||
|
|
@ -224,12 +125,12 @@ export async function initCompleteActivity(loopCtx: any, body: any): Promise<voi
|
|||
.where(eq(taskRuntime.id, TASK_ROW_ID))
|
||||
.run();
|
||||
|
||||
const history = await getOrCreateHistory(loopCtx, loopCtx.state.workspaceId, loopCtx.state.repoId);
|
||||
const history = await getOrCreateHistory(loopCtx, loopCtx.state.organizationId, loopCtx.state.repoId);
|
||||
await history.append({
|
||||
kind: "task.initialized",
|
||||
taskId: loopCtx.state.taskId,
|
||||
branchName: loopCtx.state.branchName,
|
||||
payload: { providerId },
|
||||
payload: { sandboxProviderId },
|
||||
});
|
||||
|
||||
loopCtx.state.initialized = true;
|
||||
|
|
@ -240,7 +141,7 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
|||
const detail = resolveErrorDetail(error);
|
||||
const messages = collectErrorMessages(error);
|
||||
const { config } = getActorRuntimeContext();
|
||||
const providerId = loopCtx.state.providerId ?? defaultSandboxProviderId(config);
|
||||
const sandboxProviderId = loopCtx.state.sandboxProviderId ?? defaultSandboxProviderId(config);
|
||||
|
||||
await loopCtx.db
|
||||
.insert(taskTable)
|
||||
|
|
@ -249,7 +150,7 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
|||
branchName: loopCtx.state.branchName ?? null,
|
||||
title: loopCtx.state.title ?? null,
|
||||
task: loopCtx.state.task,
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
status: "error",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
createdAt: now,
|
||||
|
|
@ -261,7 +162,7 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
|||
branchName: loopCtx.state.branchName ?? null,
|
||||
title: loopCtx.state.title ?? null,
|
||||
task: loopCtx.state.task,
|
||||
providerId,
|
||||
sandboxProviderId,
|
||||
status: "error",
|
||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||
updatedAt: now,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-nocheck
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getTaskSandbox } from "../../handles.js";
|
||||
import { resolveWorkspaceGithubAuth } from "../../../services/github-auth.js";
|
||||
import { resolveOrganizationGithubAuth } from "../../../services/github-auth.js";
|
||||
import { taskRuntime, taskSandboxes } from "../db/schema.js";
|
||||
import { TASK_ROW_ID, appendHistory, getCurrentRecord } from "./common.js";
|
||||
|
||||
|
|
@ -49,8 +49,8 @@ export async function pushActiveBranchActivity(loopCtx: any, options: PushActive
|
|||
`git push -u origin ${JSON.stringify(branchName)}`,
|
||||
].join("; ");
|
||||
|
||||
const sandbox = getTaskSandbox(loopCtx, loopCtx.state.workspaceId, activeSandboxId);
|
||||
const auth = await resolveWorkspaceGithubAuth(loopCtx, loopCtx.state.workspaceId);
|
||||
const sandbox = getTaskSandbox(loopCtx, loopCtx.state.organizationId, activeSandboxId);
|
||||
const auth = await resolveOrganizationGithubAuth(loopCtx, loopCtx.state.organizationId);
|
||||
const result = await sandbox.runProcess({
|
||||
command: "bash",
|
||||
args: ["-lc", script],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const TASK_QUEUE_NAMES = [
|
|||
"task.command.workbench.rename_task",
|
||||
"task.command.workbench.rename_branch",
|
||||
"task.command.workbench.create_session",
|
||||
"task.command.workbench.create_session_and_send",
|
||||
"task.command.workbench.ensure_session",
|
||||
"task.command.workbench.rename_session",
|
||||
"task.command.workbench.set_session_unread",
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
import { defineConfig } from "rivetkit/db/drizzle";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/actors/workspace/db/drizzle",
|
||||
schema: "./src/actors/workspace/db/schema.ts",
|
||||
});
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { actor, queue } from "rivetkit";
|
||||
import { workflow } from "rivetkit/workflow";
|
||||
import { workspaceDb } from "./db/db.js";
|
||||
import { runWorkspaceWorkflow, WORKSPACE_QUEUE_NAMES, workspaceActions } from "./actions.js";
|
||||
|
||||
export const workspace = actor({
|
||||
db: workspaceDb,
|
||||
queues: Object.fromEntries(WORKSPACE_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||
options: {
|
||||
name: "Workspace",
|
||||
icon: "compass",
|
||||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, workspaceId: string) => ({
|
||||
workspaceId,
|
||||
}),
|
||||
actions: workspaceActions,
|
||||
run: workflow(runWorkspaceWorkflow),
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue