- _supplement_decision_logs: executing evals missing a decision log get a
platform-derived execute_session (deficit) or start_analysis (all sessions
done) entry. Audit backfill only — records observable state, does not change
agent execution. Called each scan tick after requeue+scan.
- t480 legacy cron workers (5) disabled: superseded by platform-triggered
headless agent (plan C); they kept firing every minute and failing on
Channel-required.
openclaw agent has no cron state; a bare 'agenteval-intelligent-worker'
message made the worker skill decide then 'wait for the next tick',
deadlocking (task assigned, session never created). The trigger message now
demands '立即完成当前任务,不要等待下一节拍' and, when all sessions are
done, delegates to agenteval-intelligent-analyst. Verified end-to-end on
t480: 1h-window eval went executing -> session (2 real turns) -> close ->
report -> completed, fully agent-driven, no external IM channel.
OpenClaw cron requires a channel (announce->last fail-closed); webchat is a
Control-UI feature, not an addressable channel, and platform-side static
execution would degrade the intelligent eval into a static evaluation.
Solution (plan C): the platform keeps the scan loop and, when the queue has
a pending task, invokes the headless agent:
docker exec openclaw-eval openclaw agent --agent main \
-m agenteval-intelligent-worker --json
--deliver defaults to false, so no cron delivery channel is involved. The
worker skill runs unchanged under the OpenClaw agent (LLM decisions +
evaluator/analyst skills). Verified headless invocation returns ok.
scan_and_enqueue_tasks had no scheduler: the OpenClaw Worker wakes every
minute but the platform never enqueued executing evals, so the queue was
always empty. lifespan now starts an asyncio background task that scans
executing intelligent evals every 60s (aligned with the Worker wake),
cancelled cleanly on shutdown. Verified by a new startup test (879 total).
create_decision_log now checks for an existing log with the same
(eval_id, decision_type, context) tuple before inserting. If found, it
returns the existing row's dict instead of appending a duplicate. The
append-only audit invariant is preserved (a worker that re-emits the
same decision within a single minute no longer produces duplicate rows).
Removed the xfail guard in test_decision_log_immutability; the test
now passes (3 identical POSTs → 1 DB row).
AlertManager gains an optional openclaw_client. check_alerts records
each newly created alert and AlertManager.maybe_autoscale (called
from the async router for each alert) invokes cron_pool.scale_up(1).
scale_up itself caps at MAX_POOL_SIZE so repeated invocations are safe.
Removed the xfail guard in test_alert_autoscale_link; rewrote the test
to use task_backlog (duration_minutes=0) so a single check_alerts call
fires an alert and triggers auto-scale.
_send_webhook:
- Dedupe: skip if alert.webhook_sent is already True (guards against
repeated check_alerts ticks re-sending the same alert).
- Retry: up to 3 attempts with exponential backoff (1s, 2s) before
giving up. webhook_sent=True is persisted only on success.
Two xfail guards in test_openclaw_client_and_webhook now pass (876/4 xfail).
Replace read-check-write in task_queue.assign_task with UPDATE...WHERE
status='pending' and decide on rowcount so two concurrent workers
cannot both claim the same task. Also harden complete_task with the
same CAS pattern (status='assigned') so a worker + stuck-handler
double-complete leaves the DB in one state.
The xfail guard in test_worker_task_resilience now passes (4/4).
T2 frontend CronPoolMonitor: polls every 5s, unmount clears interval,
plus it.fails guard for missing visibilitychange listener (S6).
T3 router ORM contract: heartbeat updates fields + 404, decision-logs
POST persists + 404, GET lists inserted (guards S2 — must continue to
pass after router handlers move into a service in P3).
T8 decision-log immutability: append-only on context change passes;
dedupe of identical (decision_type, context) is xfail (real gap).
T7 alert→auto-scale: task_backlog alert recorded on first call (passes);
check_alerts never invokes auto_scale is xfail (real gap, §6.4).
All real gaps are logged in .scratch/v111-architecture-scan.md §6.
T4 auto-scale boundary: busy/total == 0.8 (exact threshold) must not fire
T4 auto-scale oscillation: repeated flips across 0.8 must respect MIN/MAX bounds
T9 stuck-detection critical edge: strict '<' semantics at 10min threshold
T9 detect_stuck_crons ignores non-busy: idle/stuck crons excluded even if stale
- Add metrics.py with pool utilization, task backlog, stuck rate, avg processing time, eval completion rate
- Add alerts.py with alert rules (pool utilization > 90%, task backlog > 50, stuck rate > 10%)
- Implement alert history and webhook notifications
- Add metrics and alerts APIs
- Add database migration for alert history table
- Add 11 unit tests for metrics, 10 unit tests for alerts, 8 integration tests
- Update migration tests to include new alert history table
All 853 tests passing.
- Add config_snapshot.py with save/list/get/compare functions
- Auto-save snapshots on eval creation and plan submission
- Implement snapshot query APIs (list, get single)
- Implement snapshot comparison API (diff two snapshots)
- Add 8 unit tests and 7 integration tests
Snapshots track config changes over time (created/plan_submitted/config_updated).
All 813 tests passing.
- Add OpenClawClient wrapping CLI commands (create/delete/list crons)
- Implement pool initialization, scale up/down, auto-scaling logic
- Implement cron state sync and stuck cron detection
- Add pool status and manual scaling APIs
- Add 13 unit tests and 5 integration tests
Pool automatically scales between 5-20 crons based on load.
All 778 tests passing.
Ticket 05 asks the judge review conclusions to flow into the report
verbatim; the aggregation silently dropped good/acceptable dimensions.
Collect every finding sorted poor-first and color drawer tags by rating.
E2E on t480 showed assistant bubbles rendering {'content': '...'} because
tutu replies carry msgBody as a parsed object and the router stored
str(reply.content). Coerce to the inner text before persisting.
Exploration sessions aggregate into a single exploration summary
(session counts, goal-achievement rate, issue lists from experience
records, judge conclusions when reviewed) that feeds three exits:
the campaign report gains an exploration dimension, the v0.7 analysis
stage-two input gains the summary (stats only, never full dialogues),
and the Markdown export appends a findings appendix after analysis and
comparison. With no exploration data every output stays unchanged.
Adds unit tests for the campaign report beyond value correctness:
cancellation exclusion from denominators (ADR-0004), window clamping,
naive/aware timestamp equivalence, plus schema validation of the report
dict (key sets, types, rate bounds, contiguous buckets, capability sort,
ISO-UTC timestamps) and consistency with the single aggregate_runs seam.