// @ts-nocheck import { desc, eq } from "drizzle-orm"; import { Loop } from "rivetkit/workflow"; import type { CreateTaskInput, HistoryEvent, HistoryQueryInput, ListTasksInput, SandboxProviderId, RepoOverview, RepoRecord, StarSandboxAgentRepoInput, StarSandboxAgentRepoResult, SwitchResult, TaskRecord, TaskSummary, TaskWorkbenchChangeModelInput, TaskWorkbenchCreateTaskInput, TaskWorkbenchDiffInput, TaskWorkbenchRenameInput, TaskWorkbenchRenameSessionInput, TaskWorkbenchSelectInput, TaskWorkbenchSetSessionUnreadInput, TaskWorkbenchSendMessageInput, TaskWorkbenchSessionInput, TaskWorkbenchUpdateDraftInput, WorkbenchOpenPrSummary, WorkbenchRepositorySummary, WorkbenchSessionSummary, WorkbenchTaskSummary, OrganizationEvent, OrganizationSummarySnapshot, OrganizationUseInput, } from "@sandbox-agent/foundry-shared"; import { getActorRuntimeContext } from "../context.js"; import { getGithubData, getOrCreateGithubData, getTask, getOrCreateHistory, getOrCreateRepository, selfOrganization } from "../handles.js"; import { logActorWarning, resolveErrorMessage } from "../logging.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 { organizationAppActions } from "./app-shell.js"; interface OrganizationState { organizationId: string; } interface GetTaskInput { organizationId: string; taskId: string; } interface TaskProxyActionInput extends GetTaskInput { reason?: string; } interface RepoOverviewInput { organizationId: string; repoId: string; } const ORGANIZATION_QUEUE_NAMES = [ "organization.command.createTask", "organization.command.syncGithubOrganizationRepos", "organization.command.syncGithubSession", ] as const; const SANDBOX_AGENT_REPO = "rivet-dev/sandbox-agent"; type OrganizationQueueName = (typeof ORGANIZATION_QUEUE_NAMES)[number]; export { ORGANIZATION_QUEUE_NAMES }; export function organizationWorkflowQueueName(name: OrganizationQueueName): OrganizationQueueName { return name; } const ORGANIZATION_PROFILE_ROW_ID = "profile"; 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}`); } } async function resolveRepoId(c: any, taskId: string): Promise { const row = await c.db.select({ repoId: taskLookup.repoId }).from(taskLookup).where(eq(taskLookup.taskId, taskId)).get(); if (!row) { throw new Error(`Unknown task: ${taskId} (not in lookup)`); } return row.repoId; } async function upsertTaskLookupRow(c: any, taskId: string, repoId: string): Promise { await c.db .insert(taskLookup) .values({ taskId, repoId, }) .onConflictDoUpdate({ target: taskLookup.taskId, set: { repoId }, }) .run(); } function parseJsonValue(value: string | null | undefined, fallback: T): T { if (!value) { return fallback; } try { return JSON.parse(value) as T; } catch { return fallback; } } async function collectAllTaskSummaries(c: any): Promise { const repoRows = await c.db.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl }).from(repos).orderBy(desc(repos.updatedAt)).all(); const all: TaskSummary[] = []; for (const row of repoRows) { try { 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("organization", "failed collecting tasks for repo", { organizationId: c.state.organizationId, repoId: row.repoId, error: resolveErrorMessage(error), }); } } all.sort((a, b) => b.updatedAt - a.updatedAt); return all; } function repoLabelFromRemote(remoteUrl: string): string { try { const url = new URL(remoteUrl.startsWith("http") ? remoteUrl : `https://${remoteUrl}`); const parts = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean); if (parts.length >= 2) { return `${parts[0]}/${(parts[1] ?? "").replace(/\.git$/, "")}`; } } catch { // ignore } return remoteUrl; } 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); return { id: repoRow.repoId, label: repoLabelFromRemote(repoRow.remoteUrl), taskCount: repoTasks.length, latestActivityMs, }; } function taskSummaryRowFromSummary(taskSummary: WorkbenchTaskSummary) { return { taskId: taskSummary.id, repoId: taskSummary.repoId, title: taskSummary.title, status: taskSummary.status, repoName: taskSummary.repoName, updatedAtMs: taskSummary.updatedAtMs, branch: taskSummary.branch, pullRequestJson: JSON.stringify(taskSummary.pullRequest), sessionsSummaryJson: JSON.stringify(taskSummary.sessionsSummary), }; } function taskSummaryFromRow(row: any): WorkbenchTaskSummary { return { id: row.taskId, repoId: row.repoId, title: row.title, status: row.status, repoName: row.repoName, updatedAtMs: row.updatedAtMs, branch: row.branch ?? null, pullRequest: parseJsonValue(row.pullRequestJson, null), sessionsSummary: parseJsonValue(row.sessionsSummaryJson, []), }; } async function listOpenPullRequestsSnapshot(c: any, taskRows: WorkbenchTaskSummary[]): Promise { 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 { const repoRows = await c.db .select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, updatedAt: repos.updatedAt }) .from(repos) .orderBy(desc(repos.updatedAt)) .all(); const taskRows: WorkbenchTaskSummary[] = []; for (const row of repoRows) { try { 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.organizationId, row.repoId, summary.taskId); const taskSummary = await task.getTaskSummary({}); taskRows.push(taskSummary); await c.db .insert(taskSummaries) .values(taskSummaryRowFromSummary(taskSummary)) .onConflictDoUpdate({ target: taskSummaries.taskId, set: taskSummaryRowFromSummary(taskSummary), }) .run(); } catch (error) { logActorWarning("organization", "failed collecting task summary during reconciliation", { organizationId: c.state.organizationId, repoId: row.repoId, taskId: summary.taskId, error: resolveErrorMessage(error), }); } } } catch (error) { logActorWarning("organization", "failed collecting repo during workbench reconciliation", { organizationId: c.state.organizationId, repoId: row.repoId, error: resolveErrorMessage(error), }); } } taskRows.sort((left, right) => right.updatedAtMs - left.updatedAtMs); return { 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), }; } async function requireWorkbenchTask(c: any, taskId: string) { const repoId = await resolveRepoId(c, taskId); return getTask(c, c.state.organizationId, repoId, taskId); } /** * 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 getOrganizationSummarySnapshot(c: any): Promise { const repoRows = await c.db .select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, updatedAt: repos.updatedAt, }) .from(repos) .orderBy(desc(repos.updatedAt)) .all(); const taskRows = await c.db.select().from(taskSummaries).orderBy(desc(taskSummaries.updatedAtMs)).all(); const summaries = taskRows.map(taskSummaryFromRow); return { 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), }; } async function broadcastRepoSummary( c: any, type: "repoAdded" | "repoUpdated", repoRow: { repoId: string; remoteUrl: string; updatedAt: number }, ): Promise { const matchingTaskRows = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, repoRow.repoId)).all(); const repo = buildRepoSummary(repoRow, matchingTaskRows.map(taskSummaryFromRow)); c.broadcast("organizationUpdated", { type, repo } satisfies OrganizationEvent); } async function createTaskMutation(c: any, input: CreateTaskInput): Promise { assertOrganization(c, input.organizationId); const { config } = getActorRuntimeContext(); 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(); if (!repoRow) { throw new Error(`Unknown repo: ${repoId}`); } const remoteUrl = repoRow.remoteUrl; const repository = await getOrCreateRepository(c, c.state.organizationId, repoId, remoteUrl); const created = await repository.createTask({ task: input.task, sandboxProviderId, agentType: input.agentType ?? null, explicitTitle: input.explicitTitle ?? null, explicitBranchName: input.explicitBranchName ?? null, onBranch: input.onBranch ?? null, }); await c.db .insert(taskLookup) .values({ taskId: created.taskId, repoId, }) .onConflictDoUpdate({ target: taskLookup.taskId, set: { repoId }, }) .run(); try { const task = getTask(c, c.state.organizationId, repoId, created.taskId); await organizationActions.applyTaskSummaryUpdate(c, { taskSummary: await task.getTaskSummary({}), }); } catch (error) { logActorWarning("organization", "failed seeding task summary after task creation", { organizationId: c.state.organizationId, repoId, taskId: created.taskId, error: resolveErrorMessage(error), }); } return created; } export async function runOrganizationWorkflow(ctx: any): Promise { 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) { return Loop.continue(undefined); } try { if (msg.name === "organization.command.createTask") { const result = await loopCtx.step({ name: "organization-create-task", timeout: 5 * 60_000, run: async () => createTaskMutation(loopCtx, msg.body as CreateTaskInput), }); await msg.complete(result); return Loop.continue(undefined); } if (msg.name === "organization.command.syncGithubSession") { await loopCtx.step({ name: "organization-sync-github-session", timeout: 60_000, run: async () => { const { syncGithubOrganizations } = await import("./app-shell.js"); await syncGithubOrganizations(loopCtx, msg.body as { sessionId: string; accessToken: string }); }, }); await msg.complete({ ok: true }); return Loop.continue(undefined); } if (msg.name === "organization.command.syncGithubOrganizationRepos") { await loopCtx.step({ name: "organization-sync-github-organization-repos", timeout: 60_000, run: async () => { const { syncGithubOrganizationRepos } = await import("./app-shell.js"); await syncGithubOrganizationRepos(loopCtx, msg.body as { sessionId: string; organizationId: string }); }, }); await msg.complete({ ok: true }); return Loop.continue(undefined); } } catch (error) { const message = resolveErrorMessage(error); logActorWarning("organization", "organization workflow command failed", { queueName: msg.name, error: message, }); await msg.complete({ error: message }).catch((completeError: unknown) => { logActorWarning("organization", "organization workflow failed completing error response", { queueName: msg.name, error: resolveErrorMessage(completeError), }); }); } return Loop.continue(undefined); }); } export const organizationActions = { ...organizationAppActions, async useOrganization(c: any, input: OrganizationUseInput): Promise<{ organizationId: string }> { assertOrganization(c, input.organizationId); return { organizationId: c.state.organizationId }; }, async listRepos(c: any, input: OrganizationUseInput): Promise { assertOrganization(c, input.organizationId); const rows = await c.db .select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, createdAt: repos.createdAt, updatedAt: repos.updatedAt, }) .from(repos) .orderBy(desc(repos.updatedAt)) .all(); return rows.map((row) => ({ organizationId: c.state.organizationId, repoId: row.repoId, remoteUrl: row.remoteUrl, createdAt: row.createdAt, updatedAt: row.updatedAt, })); }, async createTask(c: any, input: CreateTaskInput): Promise { const self = selfOrganization(c); return expectQueueResponse( await self.send(organizationWorkflowQueueName("organization.command.createTask"), input, { wait: true, timeout: 10_000, }), ); }, async starSandboxAgentRepo(c: any, input: StarSandboxAgentRepoInput): Promise { assertOrganization(c, input.organizationId); const { driver } = getActorRuntimeContext(); 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(), }; }, /** * 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 `getOrganizationSummary`, but only task actors should push * rows into it. */ async applyTaskSummaryUpdate(c: any, input: { taskSummary: WorkbenchTaskSummary }): Promise { await c.db .insert(taskSummaries) .values(taskSummaryRowFromSummary(input.taskSummary)) .onConflictDoUpdate({ target: taskSummaries.taskId, set: taskSummaryRowFromSummary(input.taskSummary), }) .run(); c.broadcast("organizationUpdated", { type: "taskSummaryUpdated", taskSummary: input.taskSummary } satisfies OrganizationEvent); }, async removeTaskSummary(c: any, input: { taskId: string }): Promise { await c.db.delete(taskSummaries).where(eq(taskSummaries.taskId, input.taskId)).run(); c.broadcast("organizationUpdated", { type: "taskRemoved", taskId: input.taskId } satisfies OrganizationEvent); }, async findTaskForGithubBranch(c: any, input: { repoId: string; branchName: string }): Promise<{ taskId: string | null }> { const summaries = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, input.repoId)).all(); const existing = summaries.find((summary) => summary.branch === input.branchName); return { taskId: existing?.taskId ?? null }; }, async refreshTaskSummaryForGithubBranch(c: any, input: { repoId: string; branchName: string }): Promise { const summaries = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, input.repoId)).all(); const matches = summaries.filter((summary) => summary.branch === input.branchName); for (const summary of matches) { try { const task = getTask(c, c.state.organizationId, input.repoId, summary.taskId); await organizationActions.applyTaskSummaryUpdate(c, { taskSummary: await task.getTaskSummary({}), }); } catch (error) { logActorWarning("organization", "failed refreshing task summary for GitHub branch", { organizationId: c.state.organizationId, repoId: input.repoId, branchName: input.branchName, taskId: summary.taskId, error: resolveErrorMessage(error), }); } } }, async applyOpenPullRequestUpdate(c: any, input: { pullRequest: WorkbenchOpenPrSummary }): Promise { const summaries = await c.db.select().from(taskSummaries).where(eq(taskSummaries.repoId, input.pullRequest.repoId)).all(); if (summaries.some((summary) => summary.branch === input.pullRequest.headRefName)) { return; } c.broadcast("organizationUpdated", { type: "pullRequestUpdated", pullRequest: input.pullRequest } satisfies OrganizationEvent); }, async removeOpenPullRequest(c: any, input: { prId: string }): Promise { c.broadcast("organizationUpdated", { type: "pullRequestRemoved", prId: input.prId } satisfies OrganizationEvent); }, async applyGithubRepositoryProjection(c: any, input: { repoId: string; remoteUrl: string }): Promise { const now = Date.now(); const existing = await c.db.select({ repoId: repos.repoId }).from(repos).where(eq(repos.repoId, input.repoId)).get(); await c.db .insert(repos) .values({ repoId: input.repoId, remoteUrl: input.remoteUrl, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: repos.repoId, set: { remoteUrl: input.remoteUrl, updatedAt: now, }, }) .run(); await broadcastRepoSummary(c, existing ? "repoUpdated" : "repoAdded", { repoId: input.repoId, remoteUrl: input.remoteUrl, updatedAt: now, }); }, async applyGithubDataProjection( c: any, input: { connectedAccount: string; installationStatus: string; installationId: number | null; syncStatus: string; lastSyncLabel: string; lastSyncAt: number | null; repositories: Array<{ fullName: string; cloneUrl: string; private: boolean }>; }, ): Promise { const existingRepos = await c.db.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl, updatedAt: repos.updatedAt }).from(repos).all(); const existingById = new Map(existingRepos.map((repo) => [repo.repoId, repo])); const nextRepoIds = new Set(); const now = Date.now(); for (const repository of input.repositories) { const repoId = repoIdFromRemote(repository.cloneUrl); nextRepoIds.add(repoId); await c.db .insert(repos) .values({ repoId, remoteUrl: repository.cloneUrl, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: repos.repoId, set: { remoteUrl: repository.cloneUrl, updatedAt: now, }, }) .run(); await broadcastRepoSummary(c, existingById.has(repoId) ? "repoUpdated" : "repoAdded", { repoId, remoteUrl: repository.cloneUrl, updatedAt: now, }); } for (const repo of existingRepos) { if (nextRepoIds.has(repo.repoId)) { continue; } await c.db.delete(repos).where(eq(repos.repoId, repo.repoId)).run(); c.broadcast("organizationUpdated", { type: "repoRemoved", repoId: repo.repoId } satisfies OrganizationEvent); } const profile = await c.db .select({ id: organizationProfile.id }) .from(organizationProfile) .where(eq(organizationProfile.id, ORGANIZATION_PROFILE_ROW_ID)) .get(); if (profile) { await c.db .update(organizationProfile) .set({ githubConnectedAccount: input.connectedAccount, githubInstallationStatus: input.installationStatus, githubSyncStatus: input.syncStatus, githubInstallationId: input.installationId, githubLastSyncLabel: input.lastSyncLabel, githubLastSyncAt: input.lastSyncAt, updatedAt: now, }) .where(eq(organizationProfile.id, ORGANIZATION_PROFILE_ROW_ID)) .run(); } }, async recordGithubWebhookReceipt( c: any, input: { organizationId: string; event: string; action?: string | null; receivedAt?: number; }, ): Promise { assertOrganization(c, input.organizationId); const profile = await c.db .select({ id: organizationProfile.id }) .from(organizationProfile) .where(eq(organizationProfile.id, ORGANIZATION_PROFILE_ROW_ID)) .get(); if (!profile) { return; } await c.db .update(organizationProfile) .set({ githubLastWebhookAt: input.receivedAt ?? Date.now(), githubLastWebhookEvent: input.action ? `${input.event}.${input.action}` : input.event, }) .where(eq(organizationProfile.id, ORGANIZATION_PROFILE_ROW_ID)) .run(); }, async getOrganizationSummary(c: any, input: OrganizationUseInput): Promise { assertOrganization(c, input.organizationId); return await getOrganizationSummarySnapshot(c); }, async reconcileWorkbenchState(c: any, input: OrganizationUseInput): Promise { assertOrganization(c, input.organizationId); return await reconcileWorkbenchProjection(c); }, 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 task.createWorkbenchSessionAndSend({ model: input.model, text: input.task, }); return { taskId: created.taskId }; }, async markWorkbenchUnread(c: any, input: TaskWorkbenchSelectInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.markWorkbenchUnread({}); }, async renameWorkbenchTask(c: any, input: TaskWorkbenchRenameInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.renameWorkbenchTask(input); }, async renameWorkbenchBranch(c: any, input: TaskWorkbenchRenameInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.renameWorkbenchBranch(input); }, 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 } : {}) }); }, async renameWorkbenchSession(c: any, input: TaskWorkbenchRenameSessionInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.renameWorkbenchSession(input); }, async setWorkbenchSessionUnread(c: any, input: TaskWorkbenchSetSessionUnreadInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.setWorkbenchSessionUnread(input); }, async updateWorkbenchDraft(c: any, input: TaskWorkbenchUpdateDraftInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.updateWorkbenchDraft(input); }, async changeWorkbenchModel(c: any, input: TaskWorkbenchChangeModelInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.changeWorkbenchModel(input); }, async sendWorkbenchMessage(c: any, input: TaskWorkbenchSendMessageInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.sendWorkbenchMessage(input); }, async stopWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.stopWorkbenchSession(input); }, async closeWorkbenchSession(c: any, input: TaskWorkbenchSessionInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.closeWorkbenchSession(input); }, async publishWorkbenchPr(c: any, input: TaskWorkbenchSelectInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.publishWorkbenchPr({}); }, async revertWorkbenchFile(c: any, input: TaskWorkbenchDiffInput): Promise { const task = await requireWorkbenchTask(c, input.taskId); await task.revertWorkbenchFile(input); }, async reloadGithubOrganization(c: any): Promise { await getOrCreateGithubData(c, c.state.organizationId).reloadOrganization({}); }, async reloadGithubPullRequests(c: any): Promise { await getOrCreateGithubData(c, c.state.organizationId).reloadAllPullRequests({}); }, async reloadGithubRepository(c: any, input: { repoId: string }): Promise { await getOrCreateGithubData(c, c.state.organizationId).reloadRepository(input); }, async reloadGithubPullRequest(c: any, input: { repoId: string; prNumber: number }): Promise { await getOrCreateGithubData(c, c.state.organizationId).reloadPullRequest(input); }, async listTasks(c: any, input: ListTasksInput): Promise { 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(); if (!repoRow) { throw new Error(`Unknown repo: ${input.repoId}`); } 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 { 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 repository = await getOrCreateRepository(c, c.state.organizationId, input.repoId, repoRow.remoteUrl); return await repository.getRepoOverview({}); }, async switchTask(c: any, taskId: string): Promise { const repoId = await resolveRepoId(c, taskId); const h = getTask(c, c.state.organizationId, repoId, taskId); const record = await h.get(); const switched = await h.switch(); return { organizationId: c.state.organizationId, taskId, sandboxProviderId: record.sandboxProviderId, switchTarget: switched.switchTarget, }; }, async history(c: any, input: HistoryQueryInput): Promise { assertOrganization(c, input.organizationId); const limit = input.limit ?? 20; const repoRows = await c.db.select({ repoId: repos.repoId }).from(repos).all(); const allEvents: HistoryEvent[] = []; for (const row of repoRows) { try { const hist = await getOrCreateHistory(c, c.state.organizationId, row.repoId); const items = await hist.list({ branch: input.branch, taskId: input.taskId, limit, }); allEvents.push(...items); } catch (error) { logActorWarning("organization", "history lookup failed for repo", { organizationId: c.state.organizationId, repoId: row.repoId, error: resolveErrorMessage(error), }); } } allEvents.sort((a, b) => b.createdAt - a.createdAt); return allEvents.slice(0, limit); }, async getTask(c: any, input: GetTaskInput): Promise { assertOrganization(c, input.organizationId); const repoId = await resolveRepoId(c, input.taskId); const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, repoId)).get(); if (!repoRow) { throw new Error(`Unknown repo: ${repoId}`); } 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 }> { assertOrganization(c, input.organizationId); const repoId = await resolveRepoId(c, 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 { assertOrganization(c, input.organizationId); const repoId = await resolveRepoId(c, 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 { assertOrganization(c, input.organizationId); const repoId = await resolveRepoId(c, 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 { assertOrganization(c, input.organizationId); const repoId = await resolveRepoId(c, 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 { assertOrganization(c, input.organizationId); const repoId = await resolveRepoId(c, 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 { assertOrganization(c, input.organizationId); const repoId = await resolveRepoId(c, input.taskId); const h = getTask(c, c.state.organizationId, repoId, input.taskId); await h.kill({ reason: input.reason }); }, };