mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(eval): golden-task eval harness + doctrine cohort stamp (#655)
* 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. * feat(eval): golden-task eval harness + doctrine cohort stamp roboco/eval: 6 BenchTaskSpec fixtures run through the real lifecycle in a disposable environment (the e2e_smoke harness's fake GitHub + local git origin + throwaway DB catalog — real isolation, not convention), scored deterministically (terminal status, revision_count, cycle time, tokens/cost via the agent_spawn_sessions task_id join) plus a local- model judge whose output is nested under a non_deterministic-marked object so cohort diffs don't read judge noise as regression. CLI: python -m roboco.eval run --role <slug> --cohort <name>. Source- checkout-only by declared posture (deptry-scoped ignore + a hard ImportError guard naming why; tests/ never ships in images or wheels). agent_spawn_sessions.doctrine_version (migration 081, chained on 080) is stamped at spawn-session finalize from the composed prompt layers — with the session's model column it identifies a cohort durably. Per adversarial review: bench runs patch the vault flags off (they were writing real markdown into the operator's vault), and the real-spawn OrchestratorStageSpawner is deliberately cut to NotImplementedError — spawned containers' MCP wiring resolves to the production orchestrator under real agent UUIDs, so real spawns wait for a dedicated follow-up; the injectable scripted spawner is the working path. Full suite 13852 passed / 94% coverage in the source worktree; deptry/mypy/xenon clean. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
+24
-2
@@ -198,6 +198,13 @@ select = [
|
||||
# cost; ARG001 covers FastAPI path params the fake-GitHub handlers must
|
||||
# name but not read.
|
||||
"tests/e2e_smoke/*.py" = ["PLC0415", "ARG001"]
|
||||
# The eval bench (offline CLI, not part of the served app) defers heavy/
|
||||
# optional imports — tests.e2e_smoke.harness, roboco.runtime.orchestrator,
|
||||
# roboco.services.task, asyncpg — to call time for the same reason
|
||||
# tests/e2e_smoke and roboco/services do; PLR0913 covers the stage-driving
|
||||
# and scoring call surfaces (task/spawner/role/timeout tuples), same
|
||||
# rationale as roboco/services/gateway/**.
|
||||
"roboco/eval/*.py" = ["PLC0415", "PLR0913"]
|
||||
# PTH119: _grok_usage_json sanitizes the agent id with os.path.basename — the
|
||||
# path-injection sanitizer CodeQL's query models; the pathlib equivalent
|
||||
# (Path(...).name) is not recognized by that query, so we keep os.path here.
|
||||
@@ -233,10 +240,18 @@ select = [
|
||||
# signature for keyword-argument compatibility (mypy override check), but the
|
||||
# stub bodies are empty — ARG002 would require renaming them, which breaks mypy.
|
||||
"tests/unit/services/test_optimal_grounding.py" = ["ARG002"]
|
||||
# _FakeJudge.score overrides BenchJudge.score — same override-signature
|
||||
# rationale as test_optimal_grounding.py above (a fixed fixture/diff/notes
|
||||
# stand-in body has nothing to do with those args).
|
||||
"tests/e2e_smoke/test_eval_bench.py" = ["ARG002"]
|
||||
# Collision-builder test helpers mirror the builder's many keyword inputs
|
||||
# (parent/project/intends/migration/shared/sequence) — bundling them would
|
||||
# hurt readability more than the arg count hurts.
|
||||
"tests/unit/gateway/test_collision_context.py" = ["PLR0913"]
|
||||
# _metrics()'s many optional kwargs mirror DeterministicMetrics' own field
|
||||
# count (a plain, no-defaults dataclass) — same rationale as the collision
|
||||
# builder above.
|
||||
"tests/unit/eval/test_scoring.py" = ["PLR0913"]
|
||||
|
||||
# =============================================================================
|
||||
# MyPy Configuration
|
||||
@@ -452,8 +467,15 @@ DEP002 = [
|
||||
"types-passlib",
|
||||
"types-PyYAML",
|
||||
]
|
||||
# DEP003: Starlette is a transitive dep of FastAPI, but BaseHTTPMiddleware is needed
|
||||
DEP003 = ["starlette"]
|
||||
# DEP003: Starlette is a transitive dep of FastAPI, but BaseHTTPMiddleware is needed.
|
||||
# "tests": roboco/eval/runner.py deliberately imports tests.e2e_smoke.harness/arcs
|
||||
# (the offline eval bench's disposable-project machinery — see that module's
|
||||
# docstring) — deptry sees the local `tests` package as an unresolvable
|
||||
# transitive import since it isn't a PyPI dependency at all. The runtime side
|
||||
# of this is guarded separately (an ImportError there raises a clear "this
|
||||
# needs a source checkout" error), so this is a lint-posture ignore, not a
|
||||
# correctness gap.
|
||||
DEP003 = ["starlette", "tests"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
Reference in New Issue
Block a user