mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-20 01:00:32 +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.
|
- 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.
|
- 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.
|
- 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
|
## Testing Policy
|
||||||
|
|
||||||
|
|
@ -196,6 +197,7 @@ For all Rivet/RivetKit implementation:
|
||||||
- Merge the PR.
|
- Merge the PR.
|
||||||
- Confirm the task is finished and its status is updated correctly.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
## Maintenance
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import type {
|
||||||
import { getActorRuntimeContext } from "../context.js";
|
import { getActorRuntimeContext } from "../context.js";
|
||||||
import { getOrCreateGithubState, getOrCreateHistory, getOrCreateRepository, getOrCreateTask, getTask, selfOrganization } from "../handles.js";
|
import { getOrCreateGithubState, getOrCreateHistory, getOrCreateRepository, getOrCreateTask, getTask, selfOrganization } from "../handles.js";
|
||||||
import { logActorWarning, resolveErrorMessage } from "../logging.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 { normalizeRemoteUrl, repoIdFromRemote } from "../../services/repo.js";
|
||||||
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
||||||
import { foundryRepoClonePath } from "../../services/foundry-paths.js";
|
import { foundryRepoClonePath } from "../../services/foundry-paths.js";
|
||||||
|
|
@ -468,6 +468,10 @@ export const workspaceActions = {
|
||||||
await upsertActorRuntimeIssue(c, input);
|
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 }> {
|
async useWorkspace(c: any, input: WorkspaceUseInput): Promise<{ workspaceId: string }> {
|
||||||
assertWorkspace(c, input.workspaceId);
|
assertWorkspace(c, input.workspaceId);
|
||||||
return { workspaceId: c.state.workspaceId };
|
return { workspaceId: c.state.workspaceId };
|
||||||
|
|
|
||||||
|
|
@ -806,6 +806,17 @@ export const workspaceAppActions = {
|
||||||
return await buildAppSnapshot(c, input.sessionId);
|
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 }> {
|
async beginAppGithubInstall(c: any, input: { sessionId: string; organizationId: string }): Promise<{ url: string }> {
|
||||||
assertAppWorkspace(c);
|
assertAppWorkspace(c);
|
||||||
const { profile } = await requireSignedInSession(c, input.sessionId);
|
const { profile } = await requireSignedInSession(c, input.sessionId);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { WorkflowErrorEvent } from "rivetkit/workflow";
|
import type { WorkflowErrorEvent } from "rivetkit/workflow";
|
||||||
import type { FoundryActorRuntimeIssue, FoundryActorRuntimeType } from "@sandbox-agent/foundry-shared";
|
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 { organizationActorIssues } from "./organization/db/schema.js";
|
||||||
import { getOrCreateOrganization } from "./handles.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);
|
.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 {
|
function normalizeWorkflowIssue(event: WorkflowErrorEvent): NormalizedWorkflowIssue {
|
||||||
if ("step" in event) {
|
if ("step" in event) {
|
||||||
const error = event.step.error;
|
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) => {
|
app.post("/api/rivet/app/organizations/:organizationId/reconnect", async (c) => {
|
||||||
const sessionId = await resolveSessionId(c);
|
const sessionId = await resolveSessionId(c);
|
||||||
return c.json(
|
return c.json(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export interface FoundryAppClient {
|
||||||
selectOrganization(organizationId: string): Promise<void>;
|
selectOrganization(organizationId: string): Promise<void>;
|
||||||
updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void>;
|
updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<void>;
|
||||||
triggerGithubSync(organizationId: string): Promise<void>;
|
triggerGithubSync(organizationId: string): Promise<void>;
|
||||||
|
clearOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<void>;
|
||||||
completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
||||||
openBillingPortal(organizationId: string): Promise<void>;
|
openBillingPortal(organizationId: string): Promise<void>;
|
||||||
cancelScheduledRenewal(organizationId: string): Promise<void>;
|
cancelScheduledRenewal(organizationId: string): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ export interface BackendClient {
|
||||||
selectAppOrganization(organizationId: string): Promise<FoundryAppSnapshot>;
|
selectAppOrganization(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||||
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot>;
|
updateAppOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise<FoundryAppSnapshot>;
|
||||||
triggerAppRepoImport(organizationId: string): Promise<FoundryAppSnapshot>;
|
triggerAppRepoImport(organizationId: string): Promise<FoundryAppSnapshot>;
|
||||||
|
clearAppOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<FoundryAppSnapshot>;
|
||||||
reconnectAppGithub(organizationId: string): Promise<void>;
|
reconnectAppGithub(organizationId: string): Promise<void>;
|
||||||
completeAppHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
completeAppHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void>;
|
||||||
openAppBillingPortal(organizationId: string): 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> {
|
async reconnectAppGithub(organizationId: string): Promise<void> {
|
||||||
await redirectTo(`/app/organizations/${organizationId}/reconnect`, {
|
await redirectTo(`/app/organizations/${organizationId}/reconnect`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ export interface MockFoundryAppClient {
|
||||||
selectOrganization(organizationId: string): Promise<void>;
|
selectOrganization(organizationId: string): Promise<void>;
|
||||||
updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void>;
|
updateOrganizationProfile(input: UpdateMockOrganizationProfileInput): Promise<void>;
|
||||||
triggerGithubSync(organizationId: string): Promise<void>;
|
triggerGithubSync(organizationId: string): Promise<void>;
|
||||||
|
clearOrganizationRuntimeIssues(organizationId: string, actorId?: string): Promise<void>;
|
||||||
completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void>;
|
completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void>;
|
||||||
openBillingPortal(organizationId: string): Promise<void>;
|
openBillingPortal(organizationId: string): Promise<void>;
|
||||||
cancelScheduledRenewal(organizationId: string): Promise<void>;
|
cancelScheduledRenewal(organizationId: string): Promise<void>;
|
||||||
|
|
@ -585,6 +586,21 @@ class MockFoundryAppStore implements MockFoundryAppClient {
|
||||||
this.importTimers.set(organizationId, timer);
|
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> {
|
async completeHostedCheckout(organizationId: string, planId: MockBillingPlanId): Promise<void> {
|
||||||
await this.injectAsyncLatency();
|
await this.injectAsyncLatency();
|
||||||
this.requireOrganization(organizationId);
|
this.requireOrganization(organizationId);
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,10 @@ export function createMockBackendClient(defaultWorkspaceId = "default"): Backend
|
||||||
return unsupportedAppSnapshot();
|
return unsupportedAppSnapshot();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async clearAppOrganizationRuntimeIssues(): Promise<FoundryAppSnapshot> {
|
||||||
|
return unsupportedAppSnapshot();
|
||||||
|
},
|
||||||
|
|
||||||
async reconnectAppGithub(): Promise<void> {
|
async reconnectAppGithub(): Promise<void> {
|
||||||
notSupported("reconnectAppGithub");
|
notSupported("reconnectAppGithub");
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,11 @@ class RemoteFoundryAppStore implements FoundryAppClient {
|
||||||
this.scheduleSyncPollingIfNeeded();
|
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> {
|
async completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise<void> {
|
||||||
await this.backend.completeAppHostedCheckout(organizationId, planId);
|
await this.backend.completeAppHostedCheckout(organizationId, planId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue