mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(tg): cockpit data correctness — real GLM pricing, display timezone, agent activity tracking (#666)
* fix(tg): cockpit data correctness — real GLM pricing, display timezone, agent activity tracking
Three root causes behind the Mini App/bot showing wrong numbers:
Pricing: glm-5.2 gets a grounded per-token rate (z.ai published pricing,
$1.40/$4.40/$0.26 per 1M, source+date in the table comment) so a GLM
fleet day stops reporting $0.00 for half a million tokens; ungrounded
Ollama-Cloud models render "subscription (untracked)" instead of a bare
zero (is_ollama_cloud_model, consumed directly by the cockpit). Side
effect, intended and documented: honestly-priced GLM now trips the
downgrade-only comparator for new qa/documenter complexity pins.
Display timezone: the cockpit bucketed days in UTC for a GMT+2 operator.
New pure foundation module display_time (resolve_zone/local_date/
trailing_dates/day_bounds_utc, DST-correct with tests for the 23h/25h
days) + ROBOCO_DISPLAY_TIMEZONE (IANA-validated, default UTC); the
cockpit's spend/velocity series bucket raw session/completion rows by
the display zone. The UTC-keyed rollup table and the main dashboard are
deliberately untouched.
Agent activity: AgentTable.status was never set to ACTIVE and
current_task_id was never written anywhere — "active: 0, working: []"
was structurally permanent. Every claim path now marks the claimant
ACTIVE with rollback symmetry (_finalize_claim for dev/PM claims,
_qa_or_doc_claim for QA/doc/PR-gate claims, pr_review_claim for external
review) and every release path clears it (pass/fail QA, pr_pass/pr_fail,
complete_review, advance-to-PM-review, reaper unclaim, voluntary
unclaim, reassign retarget, pool divert, admin transitions, unblock
restore-to-in-progress). The bot's /status shares the cockpit's fleet
derivation so the two surfaces can't disagree. Known ceiling, commented:
one current_task_id column shows a multi-root coordinator PM's most
recent claim only.
Drill: sonnet develop -> sonnet adversarial (refuted the original
chokepoint coverage claim; QA/doc/reviewer paths were unwired) ->
correction round (wired them all + restored a dropped assertion, deleted
a dead helper and the dead subscription_billed field) -> review.
* fix(db): post_update on AgentTable.current_task breaks the flush cycle
agents.current_task_id and tasks.assigned_to reference each other, so a
flush touching both rows — every claim now marks its agent ACTIVE — is
an instance-level circular dependency SQLAlchemy cannot topologically
sort. The e2e smoke's full verb paths (12 tests) hit it; the unit and
integration suites never flush both dirty rows with relationships
loaded. post_update emits the FK as a second UPDATE, the canonical fix
for mutually-referencing rows.
* fix(budgets): enforce only explicitly-set budgets — no per-TaskType defaults
The per-TaskType default cap table blocked an unbudgeted coordination
root one opus planning turn in ($1.50 PLANNING default vs. real
coordination spend) — a false positive by design the moment the fleet
runs a priced model. Budgets are now explicit-input only:
effective_task_budget_usd returns None for an unset budget_usd, the
budget sweep skips enforcement (and never prices spend) on None, and
the unblock re-check passes on None so clearing the budget field is
itself a valid resolution. The project monthly cap stays as the
explicit-input fleet-wide backstop. Panel copy tells the truth
("No cap" placeholder; empty = uncapped), and the TaskType default
table plus its resolver are deleted.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -56,7 +56,7 @@ const FLAG_DESCRIPTIONS: Record<string, string> = {
|
||||
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.",
|
||||
"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 explicitly-set budget 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 never capped.",
|
||||
rag_auto_update_enabled:
|
||||
"Keep the knowledge base index refreshed automatically.",
|
||||
transcript_prune_enabled:
|
||||
|
||||
@@ -81,7 +81,7 @@ function submit() {
|
||||
}
|
||||
|
||||
function budgetInput(): HTMLInputElement {
|
||||
return screen.getByPlaceholderText("Task-type default") as HTMLInputElement;
|
||||
return screen.getByPlaceholderText("No cap") as HTMLInputElement;
|
||||
}
|
||||
|
||||
describe("EditTaskDialog — Budget (USD) input", () => {
|
||||
@@ -153,7 +153,7 @@ describe("EditTaskDialog — Budget (USD) input", () => {
|
||||
expect(mutateAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("submits null when left empty (use the task-type default)", async () => {
|
||||
it("submits null when left empty (no cap)", async () => {
|
||||
render(
|
||||
<EditTaskDialog
|
||||
task={{ ...task, budget_usd: 3.5 }}
|
||||
|
||||
@@ -156,7 +156,7 @@ function EditTaskDialogInner({
|
||||
const parsedBudget = trimmedBudget ? Number(trimmedBudget) : null;
|
||||
if (trimmedBudget && (Number.isNaN(parsedBudget) || parsedBudget! <= 0)) {
|
||||
toast.error(
|
||||
"Budget must be greater than 0 — leave it empty for the task-type default",
|
||||
"Budget must be greater than 0 — leave it empty for no cap",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -359,21 +359,21 @@ function EditTaskDialogInner({
|
||||
|
||||
{/* Budget (USD) */}
|
||||
<div className="space-y-2">
|
||||
<HelpTip label="Caps this task's own agent-spawn spend; only enforced when the task-budgets feature flag is on. Empty = use the task-type default.">
|
||||
<HelpTip label="Caps this task's own agent-spawn spend; only enforced when the task-budgets feature flag is on. Empty = no cap.">
|
||||
<Label>Budget (USD)</Label>
|
||||
</HelpTip>
|
||||
<Input
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
placeholder="Task-type default"
|
||||
placeholder="No cap"
|
||||
value={budgetUsd}
|
||||
onChange={(e) => setBudgetUsd(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Must be greater than 0 — a 0 budget would block the task
|
||||
before it spends a cent. Leave blank for the task-type
|
||||
default.
|
||||
before it spends a cent. Leave blank for no cap: budgets
|
||||
enforce only when explicitly set.
|
||||
</p>
|
||||
{spendUsd != null && (
|
||||
<p className="text-xs text-muted-foreground" data-testid="task-spend">
|
||||
|
||||
@@ -228,6 +228,25 @@ describe("TgTodayTab", () => {
|
||||
expect(screen.getByText(/idle · 21/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("labels an untracked subscription-billed spend day instead of a bare $0", async () => {
|
||||
get.mockResolvedValue({
|
||||
data: brief({
|
||||
spend: {
|
||||
tokens_today: 456_221,
|
||||
cost_today_usd: 0,
|
||||
subscription_billed: true,
|
||||
series: [1, 2, 3, 4, 5, 6, 0],
|
||||
delta_pct: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
renderTab();
|
||||
|
||||
expect(await screen.findByText("≈$0")).toBeInTheDocument();
|
||||
expect(screen.getByText(/subscription \(untracked\)/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText("$0.00")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows an error state when the brief fails to load", async () => {
|
||||
get.mockRejectedValue(new Error("boom"));
|
||||
renderTab();
|
||||
|
||||
@@ -69,6 +69,11 @@ export interface TodayBrief {
|
||||
spend: {
|
||||
tokens_today: number;
|
||||
cost_today_usd: number;
|
||||
/** True when cost_today_usd is $0 because today's tokens ran on an
|
||||
* Ollama Cloud model with no grounded per-token rate (subscription-
|
||||
* billed, not literally free) — the hero renders "untracked", never a
|
||||
* bare misleading $0.00. */
|
||||
subscription_billed: boolean;
|
||||
series: number[];
|
||||
delta_pct: number | null;
|
||||
};
|
||||
@@ -134,10 +139,16 @@ function SpendHero({
|
||||
/>
|
||||
</div>
|
||||
<span className="tg-display block text-[44px] leading-none tabular-nums">
|
||||
${cost.toFixed(2)}
|
||||
{spend.subscription_billed ? "≈$0" : `$${cost.toFixed(2)}`}
|
||||
</span>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<TgDeltaChip pct={spend.delta_pct} />
|
||||
{spend.subscription_billed ? (
|
||||
<span className="text-xs font-medium text-amber-500">
|
||||
subscription (untracked)
|
||||
</span>
|
||||
) : (
|
||||
<TgDeltaChip pct={spend.delta_pct} />
|
||||
)}
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
{fmtTokens(spend.tokens_today)} tokens
|
||||
</span>
|
||||
|
||||
@@ -180,6 +180,7 @@ export const DEMO_TODAY: TodayBrief = {
|
||||
spend: {
|
||||
tokens_today: 2_400_000,
|
||||
cost_today_usd: 18.72,
|
||||
subscription_billed: false,
|
||||
series: [12.4, 9.1, 15.8, 11.2, 21.6, 14.9, 18.72],
|
||||
delta_pct: 25.6,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user