Add runtime issue clear action

This commit is contained in:
Nathan Flurry 2026-03-12 11:03:32 -07:00
parent ec8e816d0d
commit b224294b0e
11 changed files with 82 additions and 2 deletions

View file

@ -40,6 +40,7 @@ AppShellOrganization("app")
- Workflow handlers should be decomposed into small durable steps. Each local mutation or externally meaningful transition gets its own step; avoid monolithic workflow steps that bundle an entire cross-actor flow together.
- Every actor that uses `workflow(...)` must install an `onError` hook and report normalized workflow failures into organization-scoped runtime issue state.
- Organization runtime issue state is the backend source of truth for actor/workflow error badges in the frontend top bar and settings screens.
- Provide an explicit action to clear recorded organization runtime issues after investigation. Use that action instead of manual DB edits when resetting stale actor errors in dev.
## Maintenance

View file

@ -34,7 +34,7 @@ import type {
import { getActorRuntimeContext } from "../context.js";
import { getOrCreateGithubState, getOrCreateHistory, getOrCreateRepository, getOrCreateTask, getTask, selfOrganization } from "../handles.js";
import { logActorWarning, resolveErrorMessage } from "../logging.js";
import { upsertActorRuntimeIssue } from "../runtime-issues.js";
import { clearActorRuntimeIssues as clearOrganizationActorRuntimeIssues, upsertActorRuntimeIssue } from "../runtime-issues.js";
import { normalizeRemoteUrl, repoIdFromRemote } from "../../services/repo.js";
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
import { foundryRepoClonePath } from "../../services/foundry-paths.js";
@ -468,6 +468,10 @@ export const workspaceActions = {
await upsertActorRuntimeIssue(c, input);
},
async clearActorRuntimeIssues(c: any, input?: { actorId?: string | null }): Promise<void> {
await clearOrganizationActorRuntimeIssues(c, input);
},
async useWorkspace(c: any, input: WorkspaceUseInput): Promise<{ workspaceId: string }> {
assertWorkspace(c, input.workspaceId);
return { workspaceId: c.state.workspaceId };

View file

@ -806,6 +806,17 @@ export const workspaceAppActions = {
return await buildAppSnapshot(c, input.sessionId);
},
async clearAppOrganizationRuntimeIssues(c: any, input: { sessionId: string; organizationId: string; actorId?: string | null }): Promise<FoundryAppSnapshot> {
assertAppWorkspace(c);
const { profile } = await requireSignedInSession(c, input.sessionId);
requireEligibleOrganization(profile, input.organizationId);
const workspace = await getOrCreateOrganization(c, input.organizationId);
await workspace.clearActorRuntimeIssues({
actorId: input.actorId ?? null,
});
return await buildAppSnapshot(c, input.sessionId);
},
async beginAppGithubInstall(c: any, input: { sessionId: string; organizationId: string }): Promise<{ url: string }> {
assertAppWorkspace(c);
const { profile } = await requireSignedInSession(c, input.sessionId);

View file

@ -1,6 +1,6 @@
import type { WorkflowErrorEvent } from "rivetkit/workflow";
import type { FoundryActorRuntimeIssue, FoundryActorRuntimeType } from "@sandbox-agent/foundry-shared";
import { sql } from "drizzle-orm";
import { eq, sql } from "drizzle-orm";
import { organizationActorIssues } from "./organization/db/schema.js";
import { getOrCreateOrganization } from "./handles.js";
@ -98,6 +98,17 @@ export async function listActorRuntimeIssues(c: any): Promise<ActorRuntimeIssueR
.sort((left, right) => right.occurredAt - left.occurredAt);
}
export async function clearActorRuntimeIssues(c: any, input?: { actorId?: string | null }): Promise<void> {
await ensureOrganizationActorIssuesTable(c);
const actorId = input?.actorId?.trim();
if (actorId) {
await c.db.delete(organizationActorIssues).where(eq(organizationActorIssues.actorId, actorId)).run();
return;
}
await c.db.delete(organizationActorIssues).run();
}
function normalizeWorkflowIssue(event: WorkflowErrorEvent): NormalizedWorkflowIssue {
if ("step" in event) {
const error = event.step.error;

View file

@ -270,6 +270,21 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
);
});
app.post("/api/rivet/app/organizations/:organizationId/runtime-issues/clear", async (c) => {
const sessionId = await resolveSessionId(c);
const body = await c.req.json().catch(() => ({}));
return c.json(
await appWorkspaceAction(
async (workspace) =>
await workspace.clearAppOrganizationRuntimeIssues({
sessionId,
organizationId: c.req.param("organizationId"),
actorId: typeof body?.actorId === "string" ? body.actorId : null,
}),
),
);
});
app.post("/api/rivet/app/organizations/:organizationId/reconnect", async (c) => {
const sessionId = await resolveSessionId(c);
return c.json(