feat(03-03): add budget alert OS notifications and scheduled post recovery

- Add notify() calls at 80% budget threshold warning and 100% exhaustion auto-pause
- Compute percentage as Math.round(utilization * 100) for accurate display
- Add scheduled social post timer re-registration on app startup
- Add unit tests for budget threshold detection and notification logic
This commit is contained in:
ZacharyZhang-NY committed 2026-03-22 14:29:43 -04:00
1 parent 8558b113a2
commit 14da6b5ede
3 files changed
+234 -1

No files matched your search

+55 -1
View File
@@ -816,7 +816,7 @@ export function createAutomationService({
if (isSuccess && effectiveStatus === "in_progress") {
// Check if non-code task produced a deliverable before auto-advancing
const taskType = ((task as Record<string, unknown>).task_type as string) ?? "general";
const taskType = ((task as unknown as Record<string, unknown>).task_type as string) ?? "general";
const needsDeliverable = taskType === "research" || taskType === "content";
if (needsDeliverable) {
@@ -907,6 +907,19 @@ export function createAutomationService({
...automationContext,
agentId: agent.id,
});
// Fire OS notification for budget threshold warning
const pct = Math.round(utilization * 100);
notify(
"Budget Alert",
`${agent.name} is at ${pct}% of monthly budget ($${agent.spentMonthlyUsd.toFixed(2)} / $${agent.budgetMonthlyUsd.toFixed(2)})`,
);
if (utilization >= 1.0) {
// Fire OS notification for budget exhaustion (agent auto-paused by database)
notify(
"Budget Alert",
`${agent.name} paused -- monthly budget exhausted ($${agent.spentMonthlyUsd.toFixed(2)} / $${agent.budgetMonthlyUsd.toFixed(2)})`,
);
}
}
}
} catch {
@@ -981,6 +994,47 @@ export function createAutomationService({
}
}
if (approval.type === "social_post" && decision === "approved" && approval.payloadJson) {
try {
const draft = JSON.parse(approval.payloadJson) as {
socialAccountId: string;
platform: string;
content: string;
mediaUrls: string[];
scheduledAt: string | null;
};
const dispatchSocialAction = () => {
const actionId = db.saveBrowserAction({
companyId: approval.companyId,
socialAccountId: draft.socialAccountId,
agentId: approval.requestedByAgentId ?? "",
actionType: "post",
payloadJson: JSON.stringify({ content: draft.content, mediaUrls: draft.mediaUrls, scheduledAt: draft.scheduledAt }),
status: "queued",
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)}`);
});
};
if (draft.scheduledAt) {
const delay = new Date(draft.scheduledAt).getTime() - Date.now();
if (delay > 0) {
setTimeout(dispatchSocialAction, delay);
} else {
dispatchSocialAction();
}
} else {
dispatchSocialAction();
}
} catch (err) {
logger.error("[automation] Failed to dispatch approved social post:", err);
}
}
if (approval.payloadJson) {
try {
const payload = JSON.parse(approval.payloadJson) as { workflowId?: string; stepId?: string };
+32
View File
@@ -176,6 +176,38 @@ export async function bootstrapApplication({
});
}
// Re-register timers for scheduled social posts (survives app restart).
// Scan all companies for queued browser actions with a future scheduledAt time
// and set up setTimeout to dispatch them at the right moment.
try {
const snapshot = db.listSnapshot();
for (const company of snapshot.companies) {
const queuedActions = db.listBrowserActions(company.id, { status: "queued" });
for (const action of queuedActions) {
if (action.payloadJson) {
try {
const payload = JSON.parse(action.payloadJson) as { scheduledAt?: string };
if (payload.scheduledAt) {
const delay = new Date(payload.scheduledAt).getTime() - Date.now();
if (delay > 0) {
setTimeout(() => {
const browserActions = serviceGraph.getBrowserActionService();
if (browserActions) {
void browserActions.dispatchBrowserAction(action.id, company.id);
}
}, delay);
log.info(`[bootstrap] Re-registered scheduled post timer: action ${action.id} in ${Math.round(delay / 1000)}s`);
}
// If delay <= 0, the action is overdue -- normal dispatch cycle will pick it up
}
} catch { /* ignore malformed payload */ }
}
}
}
} catch (error) {
log.warn(`[bootstrap] Failed to re-register scheduled post timers: ${error instanceof Error ? error.message : String(error)}`);
}
// Show the window immediately so the user sees the app launch without delay.
await serviceGraph.getDesktopShellService()?.ensureWindow();
serviceGraph.getAppLifecycleService()?.register(hasSingleInstanceLock);