feat(05-01): budget enforcement types, schemas, IPC channels, and database CRUD

- Add BudgetPolicyRecord, BudgetIncidentRecord, BudgetStatusSummary types
- Add 6 IPC channels for budget operations in contracts.ts
- Add Zod schemas for all budget payloads
- Add DesktopApi entries for budget methods
- Add database CRUD: saveBudgetPolicy, deleteBudgetPolicy, listBudgetPolicies,
  getBudgetPolicy, getActivePoliciesForRun, getSpentInWindow (cents aggregation),
  upsertBudgetIncident (D-08 dedup), resolveIncidentsForPolicy,
  resolveWarnIncidentsForPolicy, autoCloseExpiredIncidents
- Update recordRunCompletion to include project_id in cost_entries INSERT
- Add migration 18: convert agent budgetMonthlyUsd to budget_policies rows
- Add preload bridge stubs for budget IPC channels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-26 20:47:20 -04:00
1 parent 245eb4ccf3
commit 3a68cfa707
4 files changed
+408 -3

No files matched your search

+50 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import type { AgentMessageRecord, AgentMetrics, BrowserActionRecord, CommentRecord, CompanyMetrics, DesktopEvent, InboxItem, OnboardingBootstrapResult, ProfileSnapshot, RunLogChunk, SkillImportResult, SkillRecord, SkillUpdateStatus, StandupReport } from "./types";
import type { AgentMessageRecord, AgentMetrics, BrowserActionRecord, BudgetIncidentRecord, BudgetPolicyRecord, BudgetStatusSummary, CommentRecord, CompanyMetrics, DesktopEvent, InboxItem, OnboardingBootstrapResult, ProfileSnapshot, RunLogChunk, SkillImportResult, SkillRecord, SkillUpdateStatus, StandupReport } from "./types";
export const IPC_CHANNELS = {
loadSnapshot: "agent-company:load-snapshot",
@@ -83,6 +83,12 @@ export const IPC_CHANNELS = {
listSkillAgents: "agent-company:list-skill-agents",
listAgentSkills: "agent-company:list-agent-skills",
listWakeupRequests: "agent-company:list-wakeup-requests",
saveBudgetPolicy: "agent-company:save-budget-policy",
deleteBudgetPolicy: "agent-company:delete-budget-policy",
listBudgetPolicies: "agent-company:list-budget-policies",
listBudgetIncidents: "agent-company:list-budget-incidents",
resolveBudgetIncident: "agent-company:resolve-budget-incident",
getBudgetStatus: "agent-company:get-budget-status",
} as const;
const optionalId = z.preprocess(
@@ -570,6 +576,43 @@ export const listAgentSkillsSchema = z.object({
agentId: z.string().min(1),
});
export const budgetPolicyInputSchema = z.object({
id: optionalId,
companyId: z.string().uuid(),
scopeType: z.enum(["company", "agent", "project"]),
scopeId: z.string().uuid(),
metric: z.literal("billed_cents").default("billed_cents"),
windowKind: z.literal("calendar_month_utc").default("calendar_month_utc"),
amount: z.number().int().min(1),
warnPercent: z.number().int().min(1).max(100).default(80),
hardStopEnabled: z.boolean().default(true),
notifyEnabled: z.boolean().default(true),
isActive: z.boolean().default(true),
});
export const listBudgetPoliciesSchema = z.object({
companyId: z.string().uuid(),
});
export const deleteBudgetPolicySchema = z.object({
companyId: z.string().uuid(),
policyId: z.string().uuid(),
});
export const listBudgetIncidentsSchema = z.object({
companyId: z.string().uuid(),
status: z.enum(["open", "resolved", "auto_resolved_limit_increase", "auto_closed", "all"]).default("all"),
});
export const resolveBudgetIncidentSchema = z.object({
companyId: z.string().uuid(),
incidentId: z.string().uuid(),
});
export const getBudgetStatusSchema = z.object({
companyId: z.string().uuid(),
});
export const wakeupQuerySchema = z.object({
companyId: z.string().uuid(),
status: z.enum(["queued", "claimed", "coalesced", "deferred", "completed"]).optional(),
@@ -678,6 +721,12 @@ export type DesktopApi = {
listSkillAgents: (companyId: string, skillId: string) => Promise<DesktopResult<string[]>>;
listAgentSkills: (companyId: string, agentId: string) => Promise<DesktopResult<string[]>>;
listWakeupRequests: (input: z.infer<typeof wakeupQuerySchema>) => Promise<DesktopResult<WakeupRequestRecord[]>>;
saveBudgetPolicy: (input: z.infer<typeof budgetPolicyInputSchema>) => Promise<DesktopResult<string>>;
deleteBudgetPolicy: (input: z.infer<typeof deleteBudgetPolicySchema>) => Promise<DesktopResult<boolean>>;
listBudgetPolicies: (companyId: string) => Promise<DesktopResult<BudgetPolicyRecord[]>>;
listBudgetIncidents: (input: z.infer<typeof listBudgetIncidentsSchema>) => Promise<DesktopResult<BudgetIncidentRecord[]>>;
resolveBudgetIncident: (input: z.infer<typeof resolveBudgetIncidentSchema>) => Promise<DesktopResult<boolean>>;
getBudgetStatus: (companyId: string) => Promise<DesktopResult<BudgetStatusSummary>>;
recoveryRetry: (taskId: string, agentId: string, companyId: string) => Promise<DesktopResult<null>>;
recoveryDismiss: (taskId: string) => Promise<DesktopResult<null>>;
recoveryFail: (taskId: string) => Promise<DesktopResult<null>>;
+48
View File
@@ -269,6 +269,54 @@ export interface CostRecord {
createdAt: string;
}
export interface BudgetPolicyRecord {
id: string;
companyId: string;
scopeType: "company" | "agent" | "project";
scopeId: string;
metric: string;
windowKind: string;
amount: number; // INTEGER cents
warnPercent: number;
hardStopEnabled: boolean;
notifyEnabled: boolean;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface BudgetIncidentRecord {
id: string;
companyId: string;
policyId: string;
scopeType: string;
scopeId: string;
metric: string;
windowKind: string;
windowStart: string;
windowEnd: string;
thresholdType: "warn_threshold" | "hard_stop";
amountLimit: number;
amountObserved: number;
status: "open" | "resolved" | "auto_resolved_limit_increase" | "auto_closed";
resolvedAt: string | null;
createdAt: string;
updatedAt: string;
}
export interface BudgetStatusSummary {
policies: Array<{
policyId: string;
scopeType: string;
scopeId: string;
amount: number;
spent: number;
warnPercent: number;
hardStopEnabled: boolean;
hasOpenIncident: boolean;
}>;
}
export interface ActivityRecord {
id: string;
companyId: string;