mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 18:01:30 +00:00
Add runtime issue clear action
This commit is contained in:
parent
ec8e816d0d
commit
b224294b0e
11 changed files with 82 additions and 2 deletions
|
|
@ -180,6 +180,7 @@ For all Rivet/RivetKit implementation:
|
|||
- Organization grouping is managed by the GitHub organization structure. Do not introduce a second internal grouping model that can diverge from GitHub.
|
||||
- For workflow-backed actors, install a workflow `onError` hook and report failures into organization-scoped runtime issue state so the frontend can surface actor/workflow errors without querying the entire actor tree live.
|
||||
- The main workspace top bar should make organization runtime errors obvious. If actor/workflow errors exist, show them there and include detailed issue state in settings.
|
||||
- Expose and use an explicit clear-errors action for organization runtime issues after triage. Do not reset stale actor errors by hand-editing actor DB state.
|
||||
|
||||
## Testing Policy
|
||||
|
||||
|
|
@ -196,6 +197,7 @@ For all Rivet/RivetKit implementation:
|
|||
- Merge the PR.
|
||||
- Confirm the task is finished and its status is updated correctly.
|
||||
- During this flow, verify that remote GitHub state updates correctly and that Foundry receives and applies the resulting webhook-driven state updates.
|
||||
- If the test leaves stale runtime issues behind after investigation, use the clear-errors action before the next run.
|
||||
- End-to-end testing must run against the dev backend started via `docker compose -f compose.dev.yaml up` (host -> container). Do not run E2E against an in-process test runtime.
|
||||
- E2E tests should talk to the backend over HTTP (default `http://127.0.0.1:7741/api/rivet`) and use real GitHub repos/PRs.
|
||||
- For Foundry live verification, use `rivet-dev/sandbox-agent-testing` as the default testing repo unless the task explicitly says otherwise.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface FoundryAppClient {
|
|||
selectOrganization(organizationId: string): Promise<void>;
|
||||
updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void>;
|
||||
triggerGithubSync(organizationId: string): Promise<void>;
|
||||
clearOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<void>;
|
||||
completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
||||
openBillingPortal(organizationId: string): Promise<void>;
|
||||
cancelScheduledRenewal(organizationId: string): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export interface BackendClient {
|
|||
selectAppOrganization(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot>;
|
||||
triggerAppRepoImport(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||
clearAppOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<FoundryAppSnapshot>;
|
||||
reconnectAppGithub(organizationId: string): Promise<void>;
|
||||
completeAppHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
||||
openAppBillingPortal(organizationId: string): Promise<void>;
|
||||
|
|
@ -769,6 +770,15 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
});
|
||||
},
|
||||
|
||||
async clearAppOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<FoundryAppSnapshot> {
|
||||
return await appRequest<FoundryAppSnapshot>(`/app/organizations/${organizationId}/runtime-issues/clear`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
actorId: actorId ?? null,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
async reconnectAppGithub(organizationId: string): Promise<void> {
|
||||
await redirectTo(`/app/organizations/${organizationId}/reconnect`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ export interface MockFoundryAppClient {
|
|||
selectOrganization(organizationId: string): Promise<void>;
|
||||
updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void>;
|
||||
triggerGithubSync(organizationId: string): Promise<void>;
|
||||
clearOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<void>;
|
||||
completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void>;
|
||||
openBillingPortal(organizationId: string): Promise<void>;
|
||||
cancelScheduledRenewal(organizationId: string): Promise<void>;
|
||||
|
|
@ -585,6 +586,21 @@ class MockFoundryAppStore implements MockFoundryAppClient {
|
|||
this.importTimers.set(organizationId, timer);
|
||||
}
|
||||
|
||||
async clearOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<void> {
|
||||
await this.injectAsyncLatency();
|
||||
void actorId;
|
||||
this.requireOrganization(organizationId);
|
||||
this.updateOrganization(organizationId, (organization) => ({
|
||||
...organization,
|
||||
runtime: {
|
||||
...organization.runtime,
|
||||
status: "healthy",
|
||||
errorCount: 0,
|
||||
issues: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void> {
|
||||
await this.injectAsyncLatency();
|
||||
this.requireOrganization(organizationId);
|
||||
|
|
|
|||
|
|
@ -253,6 +253,10 @@ export function createMockBackendClient(defaultWorkspaceId = "default"): Backend
|
|||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async clearAppOrganizationRuntimeIssues(): Promise<FoundryAppSnapshot> {
|
||||
return unsupportedAppSnapshot();
|
||||
},
|
||||
|
||||
async reconnectAppGithub(): Promise<void> {
|
||||
notSupported("reconnectAppGithub");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ class RemoteFoundryAppStore implements FoundryAppClient {
|
|||
this.scheduleSyncPollingIfNeeded();
|
||||
}
|
||||
|
||||
async clearOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<void> {
|
||||
this.snapshot = await this.backend.clearAppOrganizationRuntimeIssues(organizationId, actorId);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
async completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void> {
|
||||
await this.backend.completeAppHostedCheckout(organizationId, planId);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue