fix(browser): queue browser action dispatch failures

This commit is contained in:
ZacharyZhang-NY committed 2026-04-23 19:39:29 -04:00
1 parent 04c4059159
commit c26e7420fb
12 files changed
+251 -25

No files matched your search

+22 -1
View File
@@ -97,7 +97,28 @@ export function registerMiscRoutes(server: AgentApiServer) {
const onBrowserAction = server.onBrowserAction;
if (onBrowserAction) {
onBrowserAction(actionId, companyId);
try {
onBrowserAction(actionId, companyId);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
db.core.social.updateBrowserAction(actionId, {
status: "failed",
resultSummary: "Browser action dispatch failed before execution completed.",
errorMessage: message,
finishedAt: new Date().toISOString(),
});
db.core.activity.addActivity({
companyId,
actor: token.agentId,
action: "browser.action_dispatch_failed",
entityType: "browser_action",
entityId: actionId,
detail: message,
});
onDomainChanged();
server.json(res, 500, { id: actionId, status: "failed", error: "Browser action dispatch failed" });
return;
}
}
server.json(res, 201, { id: actionId, status: "queued", message: "Browser action queued for execution" });
@@ -62,7 +62,7 @@ export interface ApprovalHandlerDeps {
publishDomainChanged: () => void;
notify: (options: NotificationOptions) => void;
wakeAgentIfPossible: (agentId: string | null | undefined, companyId: string, trigger: string) => string | null;
dispatchBrowserAction: (actionId: string, companyId: string) => Promise<void>;
queueBrowserActionExecution: (actionId: string, companyId: string) => void;
isQuitting: () => boolean;
}
@@ -81,7 +81,7 @@ export function handleApprovalResolved(
decision: string,
): void {
if (deps.isQuitting()) return;
const { db, logger, publishDomainChanged, notify, wakeAgentIfPossible, dispatchBrowserAction } = deps;
const { db, logger, publishDomainChanged, notify, wakeAgentIfPossible, queueBrowserActionExecution } = deps;
const { dispatchAutomation, handleTaskStatusChange, runWorkflowPipeline, trackTimer } = callbacks;
const snapshot = db.projection.snapshot.listSnapshot();
@@ -259,9 +259,7 @@ export function handleApprovalResolved(
errorMessage: null,
});
publishDomainChanged();
void dispatchBrowserAction(browserAction.id, companyId).catch((error) => {
logger.error(`[browser] Failed to dispatch approved browser action ${browserAction.id}: ${error instanceof Error ? error.message : String(error)}`);
});
queueBrowserActionExecution(browserAction.id, companyId);
} else if (decision === "rejected") {
db.core.social.updateBrowserAction(browserAction.id, {
status: "cancelled",
@@ -301,9 +299,7 @@ export function handleApprovalResolved(
approvalId: approval.id,
});
publishDomainChanged();
void dispatchBrowserAction(actionId, approval.companyId).catch((error) => {
logger.error(`[automation] Failed to dispatch approved social post ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
});
queueBrowserActionExecution(actionId, approval.companyId);
};
if (draft.scheduledAt) {
+1 -1
View File
@@ -13,7 +13,7 @@ export interface HandlerDeps {
publishDomainChanged: () => void;
notify: (options: NotificationOptions) => void;
wakeAgentIfPossible: (agentId: string | null | undefined, companyId: string, trigger: string) => string | null;
dispatchBrowserAction: (actionId: string, companyId: string) => Promise<void>;
queueBrowserActionExecution: (actionId: string, companyId: string) => void;
isQuitting: () => boolean;
}
+3 -3
View File
@@ -21,7 +21,7 @@ export function createAutomationService({
publishDomainChanged,
notify,
wakeAgentIfPossible,
dispatchBrowserAction,
queueBrowserActionExecution,
isQuitting,
}: AutomationDependencies) {
const activeAutomationExecutions = new Set<string>();
@@ -48,7 +48,7 @@ export function createAutomationService({
publishDomainChanged,
notify,
wakeAgentIfPossible,
dispatchBrowserAction,
queueBrowserActionExecution,
isQuitting,
};
@@ -58,7 +58,7 @@ export function createAutomationService({
publishDomainChanged,
notify,
wakeAgentIfPossible,
dispatchBrowserAction,
queueBrowserActionExecution,
isQuitting,
};
+1 -1
View File
@@ -47,7 +47,7 @@ export interface AutomationDependencies {
publishDomainChanged: () => void;
notify: (options: NotificationOptions) => void;
wakeAgentIfPossible: (agentId: string | null | undefined, companyId: string, trigger: string) => string | null;
dispatchBrowserAction: (actionId: string, companyId: string) => Promise<void>;
queueBrowserActionExecution: (actionId: string, companyId: string) => void;
isQuitting: () => boolean;
}
+22 -3
View File
@@ -131,10 +131,29 @@ export async function bootstrapApplication(deps: BootstrapApplicationDependencie
app.on("before-quit", () => lifecycleProcessor.stop());
if (apiServer && serviceGraph.getBrowserActionService()) {
apiServer.setBrowserActionHandler(async (actionId, companyId) => {
apiServer.setBrowserActionHandler((actionId, companyId) => {
const browserActions = serviceGraph.getBrowserActionService();
if (!browserActions) throw new Error("Browser action service is not initialized");
await browserActions.dispatchBrowserAction(actionId, companyId);
if (!browserActions) {
const message = "Browser action service is not initialized.";
log.error(`[browser] ${message} action=${actionId}`);
db.core.social.updateBrowserAction(actionId, {
status: "failed",
resultSummary: "Browser action dispatch failed before execution completed.",
errorMessage: message,
finishedAt: new Date().toISOString(),
});
db.core.activity.addActivity({
companyId,
actor: "system",
action: "browser.action_dispatch_failed",
entityType: "browser_action",
entityId: actionId,
detail: message,
});
publishDomainChanged();
return;
}
browserActions.queueBrowserActionExecution(actionId, companyId);
});
}
@@ -9,7 +9,7 @@ export interface RecoverScheduledPostsDeps {
db: AppDatabase;
logger: Logger;
isQuittingRef: { current: boolean };
getBrowserActionService: () => { dispatchBrowserAction: (actionId: string, companyId: string) => Promise<void> } | null;
getBrowserActionService: () => { queueBrowserActionExecution: (actionId: string, companyId: string) => void } | null;
}
export function recoverScheduledPosts(deps: RecoverScheduledPostsDeps): Set<ReturnType<typeof setTimeout>> {
@@ -30,7 +30,7 @@ export function recoverScheduledPosts(deps: RecoverScheduledPostsDeps): Set<Retu
if (deps.isQuittingRef.current) return;
const browserActions = deps.getBrowserActionService();
if (browserActions) {
void browserActions.dispatchBrowserAction(action.id, company.id);
browserActions.queueBrowserActionExecution(action.id, company.id);
}
}, delay);
timers.add(timer);
+58 -1
View File
@@ -2,7 +2,7 @@ import { join } from "node:path";
import log from "electron-log/main.js";
import type { AppDatabase } from "./database";
import { BrowserManager } from "./browser-manager";
import { classifyUnknownError } from "./errors/index.js";
import { classifyUnknownError, logAppError } from "./errors/index.js";
interface BrowserActionDependencies {
db: AppDatabase;
@@ -22,6 +22,62 @@ export function createBrowserActionService({
return browserManagerRef.current;
}
function formatError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function recordDispatchFailure(actionId: string, companyId: string, error: unknown) {
const message = formatError(error);
logAppError(log, error, {
kind: "upstream",
code: "browser_action.dispatch.failed",
message: `Browser action ${actionId} dispatch failed`,
cause: error,
context: { actionId, companyId },
});
const action = db.core.social.getBrowserAction(actionId, companyId);
if (action && (action.status === "queued" || action.status === "running")) {
db.core.social.updateBrowserAction(actionId, {
status: "failed",
resultSummary: "Browser action dispatch failed before execution completed.",
errorMessage: message,
finishedAt: new Date().toISOString(),
});
}
db.core.activity.addActivity({
companyId,
actor: action?.agentId ?? "system",
action: "browser.action_dispatch_failed",
entityType: "browser_action",
entityId: actionId,
detail: message,
});
log.info("[browser] action dispatch telemetry", {
actionId,
companyId,
final_outcome: "dispatch_failed",
error: message,
});
publishDomainChanged();
}
function queueBrowserActionExecution(actionId: string, companyId: string): void {
void dispatchBrowserAction(actionId, companyId).catch((error) => {
try {
recordDispatchFailure(actionId, companyId, error);
} catch (recordError) {
logAppError(log, recordError, {
kind: "fatal",
code: "browser_action.dispatch_failure_record.failed",
message: `Failed to record browser action ${actionId} dispatch failure`,
cause: recordError,
context: { actionId, companyId, dispatchError: formatError(error) },
});
}
});
}
/**
* Returns the approval record for `approvalId`, or null if the record is
* missing. Isolates the "approval row disappeared" case so the caller does
@@ -157,6 +213,7 @@ export function createBrowserActionService({
return {
dispatchBrowserAction,
queueBrowserActionExecution,
clearSocialAccountData,
cancelAction,
launchLoginBrowser,
@@ -57,8 +57,8 @@ export function createBackgroundServices({
});
browserActionService.resetBrowserManager();
const dispatchBrowserAction = async (actionId: string, companyId: string) =>
browserActionService.dispatchBrowserAction(actionId, companyId);
const queueBrowserActionExecution = (actionId: string, companyId: string) =>
browserActionService.queueBrowserActionExecution(actionId, companyId);
const automationService = createAutomationService({
db,
@@ -68,7 +68,7 @@ export function createBackgroundServices({
notify,
wakeAgentIfPossible: (agentId, companyId, trigger) =>
orchestrationService.wakeAgentIfPossible(agentId, companyId, trigger),
dispatchBrowserAction,
queueBrowserActionExecution,
isQuitting: () => isQuittingRef.current,
});