mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(budgets): per-task and per-project cost budgets (flag-gated) (#654)
* fix(notifications): exponential backoff + CAS claim for expired-unacked re-escalation The sweep re-escalated every expired unacked ack-required notification on every ~60s tick, forever — the live incident: 3 fresh blocker escalations + Telegram DMs per minute from a static stale pile. Now each notification carries reescalation_count / last_reescalated_at / reescalation_delivered_count (migration 079): first fire at expiry, then doubling intervals from 1h capped at 24h, hard stop after ROBOCO_NOTIFICATION_MAX_REESCALATIONS (default 5) with one permanent log carrying attempts-vs-delivered so 'seen and ignored' is distinguishable from 'route never worked'. The due/wait/capped decision is a pure function in foundation/policy/communications.py. Per adversarial review, the attempt slot is claimed by compare-and-set (UPDATE ... WHERE reescalation_count = :n) BEFORE delivery — the previous draft leaned on the 60s dedup window, which never engages for BLOCKER_ESCALATION (_LOOP_PRONE_TYPES excludes it), so concurrent sweeps would have double-delivered. A lost claim skips delivery outright. Legacy rows read as count=0 and keep today's first-fire semantics. 61 tests incl. a two-session CAS race and a real alembic upgrade/downgrade round trip. * feat(budgets): per-task and per-project cost budgets (flag-gated) tasks.budget_usd + projects.monthly_budget_usd (migration 080, chained on 079; adds ix_agent_spawn_sessions_task_id since both enforcement seams filter on bare task_id). Behind ROBOCO_TASK_BUDGETS_ENABLED (default off, feature-flags card) — verifiably inert when off. Claim-time: a project-month-spend guard applies to WORK-STARTING claims only (i_will_work_on / i_will_plan) — per adversarial review, review/ doc/gate/inbound-PR claims are exempt so in-flight work can always finish reviewing and merging at cap. Spend counts closed sessions' estimated_cost_usd PLUS open sessions priced live from token snapshots (the original closed-only sum read parallel long sessions as $0). Sweep-side: the existing budget sweep also prices the active task's spend vs budget_usd (TaskType defaults when null); on breach the task is BLOCKED (HUMAN resolver, budget marker) BEFORE the graceful stop so the unclaim no-ops and the dispatcher never respawns onto it, and the CEO notification names both recovery steps. unblock on a budget-blocked task re-checks live spend and refuses while still over — no silent re-breach loop. Panel: budget inputs in both dialogs (0 rejected — a zero budget silently blocks everything), spend logic consolidated in TaskService.task_spend_usd. 42 new tests incl. a real-DB spend-query suite and a two-tick non-refire sweep test. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -176,4 +176,26 @@ describe("FeatureFlagsCard — M42 off-transition confirm + pending-keys Set", (
|
||||
expect(screen.getByText(/wikilinked Obsidian vault/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/board-review drafts/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// task_budgets_enabled joined FLAG_DESCRIPTIONS + FLAG_TOOLTIPS together —
|
||||
// guard both stay in sync the same way the vault/docs-sync case above does.
|
||||
it("renders the description and tooltip for task_budgets_enabled", async () => {
|
||||
getFeatureFlags.mockResolvedValueOnce({
|
||||
flags: [
|
||||
{
|
||||
key: "task_budgets_enabled",
|
||||
label: "Task/project cost budgets",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
note: "Changes take effect on the next backend restart.",
|
||||
});
|
||||
render(withQueryClient(<FeatureFlagsCard />));
|
||||
|
||||
expect(
|
||||
await screen.findByText(/per-project monthly and per-task cost caps/i),
|
||||
).toBeInTheDocument();
|
||||
const label = screen.getByText("Task/project cost budgets");
|
||||
expect(label.getAttribute("data-state")).toBe("closed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +55,8 @@ const FLAG_DESCRIPTIONS: Record<string, string> = {
|
||||
"Enforce a per-project architectural standard (.roboco/conventions.yml): inject the map, attach baseline constraints, and block i_am_done / pr_pass on misplaced definitions or lint suppressions.",
|
||||
possibilities_matrix_enabled:
|
||||
"When a task's work is already done (commits + open PR + all acceptance criteria addressed + no open findings), submit it for QA in one i_am_done call instead of 3-6 turns — skips the retroactive plan, journal tracing, and local quality (CI-green proxy) gates. Off by default: the standard path is unchanged until you arm this.",
|
||||
task_budgets_enabled:
|
||||
"Enforce per-project monthly and per-task cost caps (USD). A claim is refused once a project's monthly budget is reached; an active task whose own budget (or its task-type default) is breached is stopped and blocked, and you're notified. Set the caps on the project edit dialog and a task's detail page — a project/task with no cap set is unaffected either way.",
|
||||
rag_auto_update_enabled:
|
||||
"Keep the knowledge base index refreshed automatically.",
|
||||
transcript_prune_enabled:
|
||||
@@ -126,6 +128,8 @@ const FLAG_TOOLTIPS: Record<string, string> = {
|
||||
conventions_enabled: "Enforces each project's architectural placement rules.",
|
||||
possibilities_matrix_enabled:
|
||||
"Fast-paths work that's already been done elsewhere.",
|
||||
task_budgets_enabled:
|
||||
"Caps agent spend per project (monthly) and per task.",
|
||||
rag_auto_update_enabled:
|
||||
"Keeps the RAG knowledge index automatically refreshed.",
|
||||
transcript_prune_enabled:
|
||||
|
||||
Reference in New Issue
Block a user